Wednesday, February 25, 2009

BibTeX convertor

I've been thinking about using my LibraryThing account with BibTeX. The problem is, LibraryThing doesn't export to BibTeX format. So I wrote a kludgy convertor: this takes a CSV export file from LibraryThing and generates BibTeX entries from it. I'm sure it could be a zillion times better, but it seems to work pretty well under SBCL. It requires CL-PPCRE.

I apologize for the formatting, I'll fix it if I can figure it out.

;;; Attempt to actually process LibraryThing export CSV file
;;;
;;; Mark Peever
;;; 2009-02-16

;;; Requires:
;;; cl-ppcre

(defparameter library-thing-encoding :ISO-8859-1)

;; The CSV fields may contain commas
(defparameter line-pattern (concatenate 'STRING
"^\"(.+?)\"," ; title
"\"(.+?)\"," ; author-simple
"\"(.+?)\"," ; author
"([^,]*)," ; date
"([^,]*)," ; lcc
"([^,]*)," ; ddc
"([^,]*)," ; isbn
"\"(.+?)\"," ; publication
"([^,]*)," ; rating
"([^,]*)," ; review
"\"(.+?)\"," ; entry-date
"([^,]*)," ; copies
))

;; Structure to hold a book entry
(defstruct book
title
author-simple
author
date
lcc
ddc
isbn
publication-data
rating
review
entry-date
copies)

(defun create-bibtex-file (export-file bibtex-file)
"Create a BibTex file from a LibraryThing export file."
(write-data bibtex-file
(mapcar #'bibtex-entry (parse-export-file export-file))))

(defun load-data (export-file)
"Load a LibraryThing export file into a list of CSV lines. This is almost entirely a wrapper for 'fare-csv:read-csv-file."
(read-text-file export-file :format library-thing-encoding))

(defun write-data (bibtex-file entries)
"Write a list of BibTeX entries to a file."
(with-open-file (out-stream bibtex-file
:direction :output
:if-exists :append
:if-does-not-exist :create
:external-format :utf-8)
(loop for entry in entries
do (write-line entry out-stream)
finally (probe-file bibtex-file))))

(defun read-text-file (filename &key (format :utf-8))
"Read a text file into a list of lines. The EXTERNAL-FORMAT is passed as a keyword argument."
(with-open-file (istream filename
:direction :input
:external-format format)
(loop for line = (read-line istream nil)
while (not (null line))
collect line)))

(defun line->book (csv-line)
"Generate a BOOK object from a CSV line from the export file."
(cl-ppcre:register-groups-bind (title
author-simple
author
date
lcc
ddc
isbn
publication-data
rating
review
entry-date
copies)
(line-pattern csv-line)
(make-book :title title
:author-simple author-simple
:author author
:date date
:lcc lcc
:ddc ddc
:isbn isbn
:publication-data publication-data
:rating rating
:review review
:entry-date (remove-if-not #'safe-char? entry-date)
:copies copies)))

(defun parse-export-file (filename)
"Return a list of BOOKS parsed from the output file."
(remove-if-not #'book-p
(mapcar #'line->book (load-data filename))))

(defun safe-char? (char)
"Check whether CHAR is safe for use in a BibTex cite key"
(not
(find
char
'(#\. #\, #\; #\% #\" #\' #\) #\( #\# #\Space #\Tab #\NewLine))))

(defun cite-key (book)
"Create a cite-key for a book."
(concatenate 'string
(remove-if-not #'safe-char? (book-author book))
":"
(remove-if-not #'safe-char? (book-title book))
":"
(remove-if-not #'safe-char? (book-date book))))

(defun bibtex-entry (book)
"Generate a BIBTEX entry from a BOOK object."
(if (not (book-p book))
nil
(concatenate 'STRING
(format nil "@book {~A,~%" (cite-key book))
(format nil "title={~A},~%" (book-title book))
(format nil "author=~S,~%" (book-author book))
(format nil "publisher=~S,~%"
(book-publication-data book))
(format nil "date={~A} ~%" (book-date book))
"}")))


No comments: