module Data.ISBN (ISBN (..), validate, show10, show13) where import Data.Char (ord) data ISBN = ISBN Integer | NotISBN String deriving Show {- This module provides some utility functions for handling ISBNs. The function validate :: String -> ISBN attempts to parse its argument as either a 10-digit or 13-digit ISBN. If the parse fails, validate returns (NotISBN s), where s is a string explaining the parse failure. If the parse succeeds, validate returns (ISBN i), where i encodes the ISBN. It is intended that the representation of the ISBN is opaque; the data constructors are exported for use in pattern matching (e.g. "case x of ISBN i -> ...", list comprehensions). Only validate should be used for constructing ISBN values. The unusual representation of the ISBN is intended to reinforce this. The functions show10, show13 :: ISBN -> String return 10-digit and 13-digit ISBNs respectively. If the ISBN has no 10-digit version (i.e. it doesn't begin 978), show10 returns "". Unless the ISBN value was constructed by validate, there is no guarantee that these are actually valid ISBNs. Copyright 2008, Tim Goodwin. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -} validate :: String -> ISBN validate x0 = case x0 of _ | not goodChars -> invalid "only digits, X, space, and hyphen allowed" | not (is10 || is13) -> invalid "must be 10 or 13 digits" | not goodForm -> invalid "digit X only allowed as ISBN-10 check digit" | not goodCheck -> invalid "bad check digit" | otherwise -> ISBN value where invalid reason = NotISBN ("invalid ISBN `" ++ x0 ++ "': " ++ reason) x1 = filter (\c -> c `elem` "0123456789xX") x0 l = length x1 is10 = l == 10 is13 = l == 13 x2 = init x1 check = last x1 checkVal = if check == 'x' || check == 'X' then 10 else ord check - ord '0' x3 = if is10 then "978" ++ x2 else x2 value = read x3 goodChars = all (`elem` "0123456789xX -") x0 goodForm = all (`elem` "0123456789") (if is10 then x2 else x1) goodCheck = checkVal == (if is10 then check10 x2 else check13 x2) check10 i = (11 - cs 10 i `mod` 11) `mod` 11 where cs _ [] = 0 cs a (x:xs) = a * (ord x - ord '0') + cs (a-1) xs check13 x = (10 - s `mod` 10) `mod` 10 where ds = map (\c -> ord c - ord '0') x s = sum $ zipWith (*) ds ([1,3,1,3,1,3,1,3,1,3,1,3]) show10 :: ISBN -> String show10 i = case i of ISBN x | take 3 s0 == "978" -> s1 ++ check where s0 = show x s1 = drop 3 s0 c10 = check10 s1 check = if c10 == 10 then "X" else show c10 ISBN _ | otherwise -> "" NotISBN _ -> "" {- show13 assumes that an EAN (13 digit ISBN) never includes leading zeroes. At the time of writing, all EANs for books start with "978", and there is a plan to start issuing EANs that start "979", so the assumption is valid. -} show13 :: ISBN -> String show13 i = case i of ISBN x -> s ++ check where s = show x check = show (check13 s) NotISBN _ -> ""