Programming Praxis – ISBN Validation

In today’s Programming Praxis exercise, our goal is to write a number of functions related to ISBN numbers. Let’s get started, shall we?

Some imports:

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

First, we need some parsers for ISBN and EAN numbers.

isbn = (++) <$> (concat <$> sepEndBy1 (many1 d) (oneOf " -"))
            <*> option [] ([10] <$ char 'X') where
    d = read . return <$> digit
ean = string "978" *> optional (oneOf " -") *> isbn

Since we need the check digits both for validation and conversion we make separate functions for them.

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

Checking whether a number is valid is a matter of checking if the length and last digit are correct.

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

Conversion just requires changing the last digit.

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

Since I don’t like APIs that require an access key, we’ll be using openlibrary instead of isbndb.

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)

Some tests to see if everything is working properly:

main :: IO ()
main = do print $ validISBN "99921-58-10-7"
          print $ validISBN "80-902734-1-6"
          print $ validISBN "0-943396-04-2"
          print $ validISBN "0-9752298-0-X"
          print $ validISBN "0943396042"
          print $ not $ validISBN "99921-58-10-8"
          print $ not $ validISBN "99921-58-10-"
          print $ not $ validISBN "9"
          print $ validEAN "978-0-0700048-4-9"
          print $ validEAN "9780070004849"
          print $ not $ validEAN "9780070004848"
          print $ toISBN "9780070004849" == Just "0070004846"
          print $ toEAN "0070004846" == Just "978-0070004849"

          mapM_ (\(t,a) -> putStrLn ("Title: " ++ t) >>
                           putStrLn ("Authors: " ++ intercalate ", " a)) =<<
              lookupISBN "0070004846"
About these ads

Tags: , , , , , , , ,

Leave a Reply

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

WordPress.com Logo

You are commenting using your WordPress.com 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 )

Google+ photo

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

Connecting to %s


Follow

Get every new post delivered to your Inbox.

Join 35 other followers

%d bloggers like this: