{-# LANGUAGE OverloadedStrings #-}
module Data.ISBN.ISBN10
( ISBN(..)
, validateISBN10
, ISBN10ValidationError(..)
, renderISBN10ValidationError
, confirmISBN10CheckDigit
, calculateISBN10CheckDigitValue
, isbn10CharToNumericValue
, numericValueToISBN10Char
, isValidISBN10CheckDigit
, isNumericCharacter
, isISBN10
, unsafeToISBN10
) where
import Control.Monad
import Data.Char
import Data.Text ( Text )
import qualified Data.Text as Text
import Data.ISBN.Types ( ISBN (ISBN10) )
validateISBN10 :: Text -> Either ISBN10ValidationError ISBN
validateISBN10 :: Text -> Either ISBN10ValidationError ISBN
validateISBN10 Text
input = do
let inputWithoutHyphens :: Text
inputWithoutHyphens = (Char -> Bool) -> Text -> Text
Text.filter (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'-') (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Text -> Text
Text.copy Text
input
Bool
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Text -> Int
Text.length Text
inputWithoutHyphens Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
10) (Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ())
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall a b. (a -> b) -> a -> b
$
ISBN10ValidationError -> Either ISBN10ValidationError ()
forall a b. a -> Either a b
Left ISBN10ValidationError
ISBN10InvalidInputLength
let invalidBodyCharacters :: Text
invalidBodyCharacters = (Char -> Bool) -> Text -> Text
Text.filter (Bool -> Bool
not (Bool -> Bool) -> (Char -> Bool) -> Char -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Char -> Bool
isNumericCharacter) (Text -> Text
Text.init Text
inputWithoutHyphens)
Bool
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Text -> Int
Text.length Text
invalidBodyCharacters Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0) (Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ())
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall a b. (a -> b) -> a -> b
$
ISBN10ValidationError -> Either ISBN10ValidationError ()
forall a b. a -> Either a b
Left ISBN10ValidationError
ISBN10IllegalCharactersInBody
Bool
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Char -> Bool
isValidISBN10CheckDigit (Char -> Bool) -> Char -> Bool
forall a b. (a -> b) -> a -> b
$ Text -> Char
Text.last Text
inputWithoutHyphens) (Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ())
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall a b. (a -> b) -> a -> b
$
ISBN10ValidationError -> Either ISBN10ValidationError ()
forall a b. a -> Either a b
Left ISBN10ValidationError
ISBN10IllegalCharacterAsCheckDigit
Bool
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
unless (Text -> Bool
confirmISBN10CheckDigit Text
inputWithoutHyphens) (Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ())
-> Either ISBN10ValidationError ()
-> Either ISBN10ValidationError ()
forall a b. (a -> b) -> a -> b
$
ISBN10ValidationError -> Either ISBN10ValidationError ()
forall a b. a -> Either a b
Left ISBN10ValidationError
ISBN10InvalidCheckDigit
ISBN -> Either ISBN10ValidationError ISBN
forall (f :: * -> *) a. Applicative f => a -> f a
pure (ISBN -> Either ISBN10ValidationError ISBN)
-> ISBN -> Either ISBN10ValidationError ISBN
forall a b. (a -> b) -> a -> b
$ Text -> ISBN
ISBN10 Text
inputWithoutHyphens
data ISBN10ValidationError
= ISBN10InvalidInputLength
| ISBN10IllegalCharactersInBody
| ISBN10IllegalCharacterAsCheckDigit
| ISBN10InvalidCheckDigit
deriving (Int -> ISBN10ValidationError -> ShowS
[ISBN10ValidationError] -> ShowS
ISBN10ValidationError -> String
(Int -> ISBN10ValidationError -> ShowS)
-> (ISBN10ValidationError -> String)
-> ([ISBN10ValidationError] -> ShowS)
-> Show ISBN10ValidationError
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [ISBN10ValidationError] -> ShowS
$cshowList :: [ISBN10ValidationError] -> ShowS
show :: ISBN10ValidationError -> String
$cshow :: ISBN10ValidationError -> String
showsPrec :: Int -> ISBN10ValidationError -> ShowS
$cshowsPrec :: Int -> ISBN10ValidationError -> ShowS
Show, ISBN10ValidationError -> ISBN10ValidationError -> Bool
(ISBN10ValidationError -> ISBN10ValidationError -> Bool)
-> (ISBN10ValidationError -> ISBN10ValidationError -> Bool)
-> Eq ISBN10ValidationError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: ISBN10ValidationError -> ISBN10ValidationError -> Bool
$c/= :: ISBN10ValidationError -> ISBN10ValidationError -> Bool
== :: ISBN10ValidationError -> ISBN10ValidationError -> Bool
$c== :: ISBN10ValidationError -> ISBN10ValidationError -> Bool
Eq)
renderISBN10ValidationError :: ISBN10ValidationError -> Text
renderISBN10ValidationError :: ISBN10ValidationError -> Text
renderISBN10ValidationError ISBN10ValidationError
validationError =
case ISBN10ValidationError
validationError of
ISBN10ValidationError
ISBN10InvalidInputLength ->
Text
"An ISBN-10 must be 10 characters, not counting hyphens"
ISBN10ValidationError
ISBN10IllegalCharactersInBody ->
Text
"The first nine characters of an ISBN-10 must all be numbers"
ISBN10ValidationError
ISBN10IllegalCharacterAsCheckDigit ->
Text
"The last character of the supplied ISBN-10 must be a number or the letter 'X'"
ISBN10ValidationError
ISBN10InvalidCheckDigit ->
Text
"The supplied ISBN-10 is not valid"
confirmISBN10CheckDigit :: Text -> Bool
confirmISBN10CheckDigit :: Text -> Bool
confirmISBN10CheckDigit Text
isbn10 =
Text -> Int
calculateISBN10CheckDigitValue (Text -> Text
Text.init Text
isbn10) Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Char -> Int
isbn10CharToNumericValue (Text -> Char
Text.last Text
isbn10)
calculateISBN10CheckDigitValue :: Text -> Int
calculateISBN10CheckDigitValue :: Text -> Int
calculateISBN10CheckDigitValue Text
input =
Int -> String -> Int -> Int
go Int
10 (Text -> String
Text.unpack Text
input) Int
0
where
go :: Int -> String -> Int -> Int
go Int
n String
charList Int
acc =
case String
charList of
[] -> (Int
11 Int -> Int -> Int
forall a. Num a => a -> a -> a
- (Int
acc Int -> Int -> Int
forall a. Integral a => a -> a -> a
`mod` Int
11)) Int -> Int -> Int
forall a. Integral a => a -> a -> a
`mod` Int
11
Char
c:String
clist -> Int -> String -> Int -> Int
go (Int
n Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1) String
clist (Int
acc Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Char -> Int
isbn10CharToNumericValue Char
c Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
n)
isbn10CharToNumericValue :: Char -> Int
isbn10CharToNumericValue :: Char -> Int
isbn10CharToNumericValue Char
'X' = Int
10
isbn10CharToNumericValue Char
c = Char -> Int
digitToInt Char
c
numericValueToISBN10Char :: Int -> Char
numericValueToISBN10Char :: Int -> Char
numericValueToISBN10Char Int
10 = Char
'X'
numericValueToISBN10Char Int
c = Text -> Char
Text.head (Text -> Char) -> Text -> Char
forall a b. (a -> b) -> a -> b
$ String -> Text
Text.pack (String -> Text) -> String -> Text
forall a b. (a -> b) -> a -> b
$ Int -> String
forall a. Show a => a -> String
show Int
c
isValidISBN10CheckDigit :: Char -> Bool
isValidISBN10CheckDigit :: Char -> Bool
isValidISBN10CheckDigit Char
char = Char
char Char -> String -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` (String
"1234567890X" :: String)
isNumericCharacter :: Char -> Bool
isNumericCharacter :: Char -> Bool
isNumericCharacter Char
char = Char
char Char -> String -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` (String
"1234567890" :: String)
isISBN10 :: ISBN -> Bool
isISBN10 :: ISBN -> Bool
isISBN10 (ISBN10 Text
_) = Bool
True
isISBN10 ISBN
_ = Bool
False
unsafeToISBN10 :: Text -> ISBN
unsafeToISBN10 :: Text -> ISBN
unsafeToISBN10 = Text -> ISBN
ISBN10