module Data.Iban (Iban, mkIban) where import Control.Applicative ((<|>)) import Data.Attoparsec.Text as AP import Data.Char ( digitToInt, 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 t = AP.parseOnly ibanP t where ibanP = do countryCode <- AP.count 2 ibanLetter checkDigits <- AP.count 2 ibanDigit chars <- AP.many1 ibanChar endOfInput if length chars < 30 then if valid countryCode checkDigits chars then return () else fail $ "IBAN checksum does not match (" ++ T.unpack t ++ ")" else fail "IBAN has more than 34 characters" where ibanChar = ibanDigit <|> ibanLetter ibanDigit = toInteger . digitToInt <$> AP.digit ibanLetter = letterToInt <$> AP.letter letterToInt c = toInteger (ord (toUpper c) - ord 'A' + 10) charsToInteger = foldl' (\acc d -> if d >= 10 then acc * 100 + d else acc * 10 + d) 0 ibanToInteger countryCode checkDigits chars = charsToInteger chars * 1000000 + charsToInteger countryCode * 100 + charsToInteger checkDigits valid countryCode checkDigits chars = ibanToInteger countryCode checkDigits chars `mod` 97 == 1