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)))))
          (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; 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 server, executes the query, then reads the result line-by-line looking for the needed information:

(define (lookup-isbn isbn)
    (string-append ""
      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)))
        (when (and (< 13 (string-length str))
                   (string=? (substring str 0 13) ""))
          (display "Authors: ")
          (display (substring str 13 (- (string-length str) 14)))
          (newline)))))) provides much more than just author and title, which you can see with this function:

(define ( isbn)
    (string-append ""
      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


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. […]

  2. My Haskell solution (see 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 . ("\
                        \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+"$@"}
    #lang racket
    (require rackunit rackunit/text-ui)
    (define (digitchar->number d)
      (if (char=? (char-downcase d) #\x)
          (- (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))
        [(list "978" region publisher title check)
         (zero? (remainder
                 (->checksum (build-list 10 (lambda (i) (if (even? i ) 1 3)))
                             (->digits region publisher title check))
        [_ #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
    (provide main)
    (define (main . args)
      (exit (run-tests all-tests 'verbose)))

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: