ISBN Validation
May 20, 2011
The dashes and spaces in ISBN and EAN numbers are a nuisance, so our first function gets rid of them:
(define (clean isbn? str)
(let loop ((cs (string->list str)) (out '()))
(cond ((null? cs) (reverse out))
((and isbn? (null? (cdr cs)) (char-ci=? (car cs) #\X))
(reverse (cons (car cs) out)))
((char-numeric? (car cs))
(loop (cdr cs) (cons (car cs) out)))
(else (loop (cdr cs) out)))))
We validate ISBN numbers according to the method given above, being careful to handle the X check digit properly:
(define (isbn? str)
(let loop ((cs (clean #t str)) (mul 10) (sum 0))
(if (null? cs) (zero? (modulo sum 11))
(loop (cdr cs) (- mul 1) (+ sum (* mul
(if (char-ci=? (car cs) #\X) 10
(- (char->integer (car cs)) 48))))))))
Validating an EAN is simpler because everything is a digit; the expression (- 4 mul) alternates the multiplier between 1 and 3:
(define (ean? str)
(let loop ((cs (clean #f str)) (mul 1) (sum 0))
(if (null? cs) (zero? (modulo sum 10))
(loop (cdr cs) (- 4 mul) (+ sum (* mul
(- (char->integer (car cs)) 48))))))))
Converting back and forth between ISBN and EAN isn’t hard, though it can be confusing to keep track of all the special constants involved:
(define (isbn->ean str)
(if (not (isbn? str)) (error 'isbn->ean "invalid isbn")
(let loop ((cs (clean #t str)) (ean '(#\8 #\7 #\9))
(mul 3) (sum 38))
(if (null? (cdr cs))
(list->string (reverse (cons (integer->char (+
(modulo (- 10 (modulo sum 10)) 10) 48)) ean)))
(loop (cdr cs) (cons (car cs) ean) (- 4 mul)
(+ sum (* mul (- (char->integer (car cs)) 48))))))))
(define (ean->isbn str)
(if (not (ean? str)) (error 'ean->isbn "invalid ean")
(let loop ((cs (drop 3 (clean #f str))) (isbn '())
(mul 10) (sum 0))
(if (null? (cdr cs))
(list->string (reverse
(cons (let ((d (modulo sum 11)))
(cond ((= d 0) #) ((= d 1) #\X)
(else (integer->char (- 59 d)))))
isbn)))
(loop (cdr cs) (cons (car cs) isbn) (- mul 1)
(+ sum (* mul (- (char->integer (car cs)) 48))))))))
To lookup the author and title, we steal the with-input-from-url function of a previous exercise. The access key is provided by isbndb.com; you’ll have to contact them for your own:
(define access-key "12345678") ; not a valid access key
Here is the function to look up the author and title. It builds the query string to be sent to the isbndb.com server, executes the query, then reads the result line-by-line looking for the needed information:
(define (lookup-isbn isbn)
(with-input-from-url
(string-append "http://isbndb.com/api/books.xml?access_key="
access-key "&index1=isbn&value1="
(list->string (clean #t isbn)))
(lambda ()
(do ((str (read-line) (read-line))) ((eof-object? str))
(when (and (< 7 (string-length str))
(string=? (substring str 0 7) "<Title>"))
(display "Title: ")
(display (substring str 7 (- (string-length str) 8)))
(newline))
(when (and (< 13 (string-length str))
(string=? (substring str 0 13) ""))
(display "Authors: ")
(display (substring str 13 (- (string-length str) 14)))
(newline))))))
Isbndb.com provides much more than just author and title, which you can see with this function:
(define (display-isbndb.com isbn)
(with-input-from-url
(string-append "http://isbndb.com/api/books.xml?access_key="
access-key "&index1=isbn&value1="
(list->string (clean #t isbn)))
(lambda ()
(do ((c (read-char) (read-char))) ((eof-object? c))
(display c)))))
Here’s an example:
> (lookup-isbn (ean->isbn "978-0070004849"))
Title: Structure and interpretation of computer programs
Authors: Harold Abelson and Gerald Jay Sussman, with Julie Sussman; foreword by Alan J. Perlis
We used drop and read-line from the Standard Prelude. You can run the program at http://programmingpraxis.codepad.org/vxKNN1KE.
[…] today’s Programming Praxis exercise, our goal is to write a number of functions related to ISBN numbers. […]
My Haskell solution (see http://bonsaicode.wordpress.com/2011/05/20/programming-praxis-isbn-validation/ for a version with comments):
import Control.Applicative hiding ((<|>), optional) import Data.Char import Data.List import Data.Map (elems) import Network.HTTP import Text.HJson import Text.HJson.Query import Text.Parsec isbn = (++) <$> (concat <$> sepEndBy1 (many1 d) (oneOf " -")) <*> option [] ([10] <$ char 'X') where d = read . return <$> digit ean = string "978" *> optional (oneOf " -") *> isbn isbnCheck, eanCheck :: Integral a => [a] -> a isbnCheck n = 11 - mod (sum $ zipWith (*) [10,9..] (take 9 n)) 11 eanCheck n = mod (sum $ zipWith (*) (cycle [1,3]) (take 9 n)) 10 validISBN, validEAN :: String -> Bool validISBN = valid isbn isbnCheck validEAN = valid ean eanCheck valid p c = either (const False) v . parse p "" where v ds = length ds == 10 && c ds == last ds toISBN, toEAN :: String -> Maybe String toISBN = convert ean isbnCheck toEAN = fmap ("978-" ++) . convert isbn eanCheck convert p c = either (const Nothing) (Just . fixCheck) . parse p "" where fixCheck n = map intToDigit (init n) ++ [check $ c n] check n = if n == 10 then 'X' else intToDigit n lookupISBN :: String -> IO [(String, [String])] lookupISBN = get . ("http://openlibrary.org/api/books?format=json&\ \jscmd=data&bibkeys=ISBN:" ++) where f ~(JObject j) = map (\b -> (unjs $ key "title" b, map (unjs . key "name") . getFromArr $ key "authors" b)) $ elems j key k = head . getFromKey k unjs ~(JString s) = s get url = fmap (either (const undefined) f . fromString) . getResponseBody =<< simpleHTTP (getRequest url)My Python submission
For the EAN validation, I opted against using
from itertools import cycleto get a repeating list[1, 3, 1, 3,...], instead using2 + (-1)**xasxcounted up.Just the verification bits, in PLT racket:
#! /bin/sh #| Hey Emacs, this is -*-scheme-*- code! exec racket --require "$0" --main -- ${1+"$@"} |# ;; https://programmingpraxis.com/2011/05/20/isbn-validation/ #lang racket (require rackunit rackunit/text-ui) (define (digitchar->number d) (if (char=? (char-downcase d) #\x) 10 (- (char->integer d) (char->integer #\0)))) (define (groups str) (regexp-split #rx"[- \t]+" str)) (define (->digits . strings) (map digitchar->number (append* (map string->list strings)))) (define (->checksum constant digits) (apply + (map * digits constant))) (provide validate-ISBN/EAN) (define (validate-ISBN/EAN str) (match (groups str) [(list region publisher title check) (zero? (remainder (->checksum (build-list 10 (curry - 10)) (->digits region publisher title check)) 11))] [(list "978" region publisher title check) (zero? (remainder (->checksum (build-list 10 (lambda (i) (if (even? i ) 1 3))) (->digits region publisher title check)) 10))] [_ #f])) (define-test-suite validate-ISBN/EAN-tests (check-true (validate-ISBN/EAN "0-330-28987-X")) (check-true (validate-ISBN/EAN "0- 330 -28987--X")) (check-false (validate-ISBN/EAN "1-330-28987-X")) (check-false (validate-ISBN/EAN "frotz plotz")) (check-true (validate-ISBN/EAN "978-0-440-22378-8")) (check-false (validate-ISBN/EAN "978-0-441-22378-8"))) (define-test-suite all-tests validate-ISBN/EAN-tests) (provide main) (define (main . args) (exit (run-tests all-tests 'verbose)))