## 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.

Pages: 1 2

### 4 Responses to “ISBN Validation”

1. […] today’s Programming Praxis exercise, our goal is to write a number of functions related to ISBN numbers. […]

```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 [] ( <\$ 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)
```
3. Graham said

My Python submission
For the EAN validation, I opted against using `from itertools import cycle` to get a repeating list `[1, 3, 1, 3,...]`, instead using `2 + (-1)**x` as `x` counted up.

4. Eric Hanchrow said

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)))
```