summaryrefslogtreecommitdiffstats
path: root/app/Data/Iban.hs
blob: 412577ae39c22fffa80237697de22923db558c84 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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