module Data.Iban (Iban, mkIban) where import Control.Applicative ((<|>)) import Data.Attoparsec.Text as AP import Data.Char ( digitToInt, isAscii, isDigit, ord, toUpper, ) import Data.Text qualified as T newtype Iban = Iban T.Text deriving (Show, Eq) mkIban :: T.Text -> Either String Iban mkIban t = validateIban t >> return (Iban t) validateIban :: T.Text -> Either String () validateIban = AP.parseOnly $ do countryCode <- AP.count 2 AP.letter checkDigits <- AP.count 2 AP.digit chars <- AP.many1 (AP.letter <|> AP.digit) endOfInput if length chars < 30 then if valid countryCode checkDigits chars then return () else fail $ "IBAN checksum does not match (" ++ countryCode ++ checkDigits ++ chars ++ ")" else fail "IBAN has more than 34 characters" where letterToInt c = ord (toUpper c) - ord 'A' + 10 charsToInteger = foldl' ( \acc -> \case d | isDigit d -> acc * 10 + toInteger (digitToInt d) | isAscii d -> acc * 100 + toInteger (letterToInt d) | otherwise -> error "unreachable" ) 0 ibanToInteger countryCode checkDigits chars = charsToInteger chars * 1000000 + charsToInteger countryCode * 100 + charsToInteger checkDigits valid countryCode checkDigits chars = ibanToInteger countryCode checkDigits chars `mod` 97 == 1