{- | Module : Luhn Description : An implementation of Luhn's check digit algorithm. Copyright : (c) N-Sim Ltd. 2008 License : BSD3 Maintainer : jhb@n-sim.com Stability : provisional Portability : portable An implementation of Luhn's check digit algorithm. -} module Luhn( -- * Creating a check digit addLuhnDigit, -- * Validating a check digit checkLuhnDigit, -- * QuickCheck tests prop_checkLuhn, prop_checkSingleError ) where import Test.QuickCheck -- | Like Python's enumerate function - returns a tuple where the first -- element is the index from 0 of the second element in the input list. enumerate :: Integral n => [a] -> [(n, a)] enumerate xs = enumerate' 0 xs where enumerate' _ [] = [] enumerate' counter (a:as) = (counter, a) : enumerate' (counter + 1) as -- | Returns the digits of a number as a list, in reverse order. digitsRev :: Integral n => n -> [n] digitsRev i = case i of 0 -> [] _ -> lastDigit : digitsRev rest where (rest, lastDigit) = quotRem i 10 -- | Returns the digits of a number as a list. digits :: Integral n => n -> [n] digits = reverse . digitsRev -- | Takes a list of digits, and converts them back into a number. unDigits :: Integral n => [n] -> n unDigits = foldl (\ a b -> a * 10 + b) 0 -- | Appends a Luhn check digit to the end of a number. addLuhnDigit :: Integral n => n -- ^ Number to which a Luhn check digit will be appended. -> n -- ^ Number with the appended Luhn check digit. addLuhnDigit num = num * 10 + checkDigit where checkDigit = (10 - total `mod` 10) `mod` 10 total = sum $ concat $ map doubleEven (enumerate $ digitsRev num) doubleEven :: Integral n => (Int, n) -> [n] doubleEven (i, n) = if odd i then [n] else digitsRev (2 * n) -- | Validates that the Luhn check digit (assumed to be the last/least- -- significant digit in the number) is correct. checkLuhnDigit :: Integral n => n -- ^ Number with a Luhn check digit as its last digit. -> Bool -- ^ Whether or not the check digit is consistent. checkLuhnDigit num = total `mod` 10 == 0 where total = sum $ concat $ map doubleOdd (enumerate $ digitsRev num) doubleOdd :: Integral n => (Int, n) -> [n] doubleOdd (i, n) = if odd i then digitsRev (2 * n) else [n] -- | Validates that a generated check digit validates. prop_checkLuhn :: Integer -- ^ Number to validate a Luhn check digit for. -> Property prop_checkLuhn i = i > 0 ==> (checkLuhnDigit . addLuhnDigit) i -- | Any single number transcription error should result in a failure in -- the validation of a Luhn check digit. This property validates this. prop_checkSingleError :: Integer -- ^ The number to transcribe. -> Integer -- ^ The position to introduce a transcription error. -> Integer -- ^ The number to transcribe in place of the original. -> Property prop_checkSingleError i modDigit replace = i > 0 ==> let checkNum = addLuhnDigit i checkDigits = digits checkNum modDigit' = modDigit `mod` fromIntegral (length checkDigits - 1) start = take (fromIntegral modDigit') checkDigits rest = drop (fromIntegral (modDigit' + 1)) checkDigits newDigits = start ++ [replace `mod` 10] ++ rest newNum = unDigits newDigits in (newNum == checkNum) || (not $ checkLuhnDigit newNum)