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 = 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 | '0' <= d && d <= '9' -> acc * 10 + toInteger (digitToInt d) | 'A' <= d && d <= 'Z' || 'a' <= d && d <= 'z' -> 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