summaryrefslogtreecommitdiffstats
path: root/app/Data/Iban.hs
blob: a42e1924d9c220f2816fb0e88bc0bc9e2de90715 (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
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