## Posts Tagged ‘ean’

### Programming Praxis – ISBN Validation

May 20, 2011

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