summaryrefslogtreecommitdiffstats
path: root/lib/iban.ml
blob: 6e47e9d8a23c5f25988c96cadce7b60a64481d73 (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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
open Core
open Option.Let_syntax

type t = string

(* Modulo-97 arithmetic. Prevents us from having to use Zarith here. *)
module M97 : sig
  type t

  val of_int : int -> t
  val lt : t -> t -> bool
  val equal : t -> t -> bool
  val ( * ) : t -> t -> t
  val ( + ) : t -> t -> t
  val ( ~$ ) : int -> t
end = struct
  type t = int

  let of_int x = x % 97
  let equal = Int.( = )
  let lt = Int.( < )
  let ( * ) x y = x * y % 97
  let ( + ) x y = (x + y) % 97
  let ( ~$ ) = of_int
end

let m97_of_alnum c =
  let v = Char.to_int c in
  if Char.is_digit c then Some (M97.of_int (v - Char.to_int '0'))
  else if Char.is_alpha c then
    if Char.is_lowercase c then Some (M97.of_int (v - Char.to_int 'a' + 10))
    else Some (M97.of_int (v - Char.to_int 'A' + 10))
  else None

let m97_of_digit c =
  match m97_of_alnum c with Some v when M97.(lt v ~$10) -> Some v | _ -> None

let m97_of_alpha c =
  match m97_of_alnum c with
  | Some v when not M97.(lt v ~$10) -> Some v
  | _ -> None

let string_fold_option ~(init : 'a) ~(f : 'a -> char -> 'a option) s =
  let rec go i (acc : 'a) : 'a option =
    if i >= String.length s then Some acc
    else Option.(f acc (String.unsafe_get s i) >>= go (i + 1))
  in
  go 0 init

let m97_of_iban s =
  string_fold_option s ~init:`In_country1 ~f:(fun st c ->
      match st with
      | `In_country1 ->
          let%map co1 = m97_of_alpha c in
          `In_country2 co1
      | `In_country2 co1 ->
          let%map co2 = m97_of_alpha c in
          `In_check1 M97.((co1 * ~$100) + co2)
      | `In_check1 co ->
          let%map ch1 = m97_of_digit c in
          `In_check2 (co, ch1)
      | `In_check2 (co, ch1) ->
          let%map ch2 = m97_of_digit c in
          `In_bban M97.(co, (ch1 * ~$10) + ch2, ~$0)
      | `In_bban (co, ch, bban) ->
          let%map v = m97_of_alnum c in
          let bban' =
            M97.(if lt v ~$10 then (bban * ~$10) + v else (bban * ~$100) + v)
          in
          `In_bban (co, ch, bban'))
  |> function
  | Some (`In_bban (co, ch, bban)) ->
      Some M97.((bban * ~$1000000) + (co * ~$100) + ch)
  | _ -> None

let check_iban s =
  String.length s <= 34 && Option.exists (m97_of_iban s) ~f:M97.(equal ~$1)

let make s : t option = if check_iban s then Some s else None
let to_string = Fn.id

let of_string s =
  match make s with
  | Some iban -> iban
  | None -> Printf.failwithf "Iban.of_string: %S" s ()

let equal = String.equal