{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} module Data.ISBN ( -- * Introduction -- $introduction -- * Documentation ISBN -- $isbn-import , renderISBN -- * ISBN Validation , validateISBN -- *** ISBN Validation Errors , ISBNValidationError(..) , renderISBNValidationError -- * Validating only ISBN-10 , validateISBN10 -- *** ISBN-13 Validation Errors , ISBN10ValidationError , renderISBN10ValidationError -- * Validating only ISBN-13 , validateISBN13 -- *** ISBN-10 Validation Errors , ISBN13ValidationError(..) , renderISBN13ValidationError -- * Conversion between ISBN-10 and ISBN-13 -- $conversion , convertISBN10toISBN13 , convertISBN13toISBN10 -- * ISBN Helpers , isISBN10 , isISBN13 -- * Creating ISBN values without validation -- $unsafe , unsafeToISBN10 , unsafeToISBN13 ) where import Data.ISBN.ISBN10 import Data.ISBN.ISBN13 import Control.Monad import Data.Text as Text -- $introduction -- -- This library contains tools for validating and working with -- [International Standard Book Numbers (ISBNs)](https://en.wikipedia.org/wiki/International_Standard_Book_Number). -- An ISBN value can be in either the format of a ten digit ISBN-10 or a -- thirteen digit ISBN-13. This library uses a single 'ISBN' data type with two -- value constructors to represent ISBN values, so that a single entry point can -- be used to validate text inputs as either an 'ISBN10', 'ISBN13', or neither. -- -- For most users, importing only the @Data.ISBN@ module is necessary, as this -- module re-exports all of the data types and functions necessary for -- validating, creating, and converting between ISBN values. ------------------------------------ -- $isbn-import -- -- __NOTE:__ This module does not export the constructors in the @ISBN@ type. -- For access to the constructors, import the @Data.Types.ISBN@ module. ------------------------------------ -- | Used to safely create 'ISBN' values. Assumes that the 'Data.Text.Text' -- input is an ISBN-10 or ISBN-13 string, either with or without hyphens. -- -- Will return either a validated @ISBN@ or an 'ISBNValidationError', which can be -- rendered as a descriptive string using 'renderISBNValidationError'. -- -- /Examples:/ -- -- @ -- validateISBN "0345816021" == Right (ISBN10 "0345816021") -- validateISBN "0-807-01429-X" == Right (ISBN10 "080701429X") -- validateISBN "9780807014295" == Right (ISBN13 "9780807014295") -- validateISBN "978-0-306-40615-7" == Right (ISBN13 "9780306406157") -- validateISBN "0-345-816" == Left InvalidISBNInputLength -- validateISBN "X-345-81602-1" == Left IllegalCharactersInISBN10Body -- validateISBN "0-345-81602-B" == Left IllegalCharacterAsISBN10CheckDigit -- validateISBN "0-345-81602-3" == Left InvalidISBN10CheckDigit -- validateISBN "00000000000000" == Left InvalidISBNInputLength -- validateISBN "9780807014299" == Left InvalidISBN13CheckDigit -- validateISBN "0X00000000000" == Left IllegalCharactersInISBN13Input -- @ validateISBN :: Text -> Either ISBNValidationError ISBN validateISBN isbn = do let isbn10result = validateISBN10 isbn isbn13result = validateISBN13 isbn case (isbn10result, isbn13result) of (Right isbn10, _) -> Right isbn10 (_, Right isbn13) -> Right isbn13 (Left ISBN10InvalidInputLength, Left ISBN13InvalidInputLength) -> Left InvalidISBNInputLength (Left ISBN10IllegalCharactersInBody, _) -> Left IllegalCharactersInISBN10Body (Left ISBN10IllegalCharacterAsCheckDigit, _) -> Left IllegalCharacterAsISBN10CheckDigit (_ , Left ISBN13IllegalCharactersInInput) -> Left IllegalCharactersInISBN13Input (Left ISBN10InvalidCheckDigit, _) -> Left InvalidISBN10CheckDigit (_, Left ISBN13InvalidCheckDigit) -> Left InvalidISBN13CheckDigit -- | Convert an 'ISBN' value to a 'Text' string. Can be used when displaying an -- ISBN in an application interface or before storing the plain ISBN text values -- in a database. -- -- 'ISBN' values created using 'validateISBN', 'validateISBN10', or -- 'validateISBN13' will never contain hyphens. -- -- /Examples:/ -- -- @ -- renderISBN (ISBN10 "080701429X") == "080701429X" -- renderISBN (ISBN13 "9780060899226") == "9780060899226" -- @ renderISBN :: ISBN -> Text renderISBN (ISBN10 i) = i renderISBN (ISBN13 i) = i -- | Possible validation errors resulting from ISBN validation. Can be -- rendered as a descriptive error message using 'renderISBNValidationError'. data ISBNValidationError = InvalidISBNInputLength -- ^ The length of the input string is not 10 or 13 characters, not counting hyphens | IllegalCharactersInISBN10Body -- ^ The first nine characters of the ISBN-10 input contain non-numeric characters | IllegalCharactersInISBN13Input -- ^ The ISBN-13 input contains non-numeric characters | IllegalCharacterAsISBN10CheckDigit -- ^ The check digit of the ISBN-10 is not a valid character (@0-9@ or @\'X\'@) | InvalidISBN10CheckDigit -- ^ The check digit is not valid for the given ISBN-10 | InvalidISBN13CheckDigit -- ^ The check digit is not valid for the given ISBN-13 deriving (Show, Eq) -- | Convert an 'ISBNValidationError' into a human-friendly error message. renderISBNValidationError :: ISBNValidationError -> Text renderISBNValidationError validationError = case validationError of InvalidISBNInputLength -> "An ISBN must be 10 or 13 characters, not counting hyphens" IllegalCharactersInISBN10Body -> "The first nine non-hyphen characters of an ISBN-10 must all be numbers" IllegalCharactersInISBN13Input -> "Every non-hyphen character of an ISBN-13 must be a number" IllegalCharacterAsISBN10CheckDigit -> "The last character of an ISBN-10 must be a number or the letter 'X'" InvalidISBN10CheckDigit -> "The supplied ISBN-10 is not valid" InvalidISBN13CheckDigit -> "The supplied ISBN-13 is not valid" -- $conversion -- -- ISBN values can be converted from ISBN-10 to ISBN-13 and vise versa. -- | Convert an ISBN-10 to an ISBN-13. Since all ISBN-10s can be converted to -- ISBN-13s, this operation cannot fail. -- -- /Example:/ -- -- @ -- convertISBN10toISBN13 (unsafeToISBN10 "0060899220") == ISBN13 "9780060899226" -- @ convertISBN10toISBN13 :: ISBN -> ISBN convertISBN10toISBN13 isbn10 = unsafeToISBN13 $ isbn13Body <> isbn13CheckDigit where isbn13CheckDigit = Text.singleton . numericValueToISBN13Char $ calculateISBN13CheckDigitValue isbn13Body isbn13Body = "978" <> isbn10Body isbn10Body = Text.init $ renderISBN isbn10 -- | Convert an ISBN-13 to an ISBN-10. Since only ISBN-13s starting with '978' -- can be converted, this operation may fail. -- -- /Example:/ -- -- @ -- convertISBN13toISBN10 (unsafeToISBN13 "9780060899226") == Just (ISBN10 "0060899220") -- @ convertISBN13toISBN10 :: ISBN -> Maybe ISBN convertISBN13toISBN10 isbn13 = do let isbn13Text = renderISBN isbn13 unless ("978" `isPrefixOf` isbn13Text) Nothing -- "Only ISBN-13s that begin with '978' can be converted to ISBN-10s" let isbn10Body = Text.init $ Text.drop 3 isbn13Text isbn10CheckDigit = Text.singleton . numericValueToISBN10Char $ calculateISBN10CheckDigitValue isbn10Body pure $ unsafeToISBN10 $ isbn10Body <> isbn10CheckDigit -- $unsafe -- -- In most cases, creating 'ISBN10' and 'ISBN13' values should be performed -- using the 'validateISBN', 'validateISBN10', or 'validateISBN13' functions, -- which ensure the ISBN values they produce are valid. -- -- The functions below allow for the creation of ISBN values without any -- validation. They should only be used in specific cases. For example, when -- loading already-validated ISBN values stored in a text column in a database.