summaryrefslogtreecommitdiffstats
path: root/lib/iban.ml
diff options
context:
space:
mode:
authorRutger Broekhoff2025-08-25 19:48:19 +0200
committerRutger Broekhoff2025-08-25 19:48:19 +0200
commit95d50b25c990e8c945ce2507b16ff3c8b039d286 (patch)
treec1ff4c7f9601c6980eed1a7235ba336c5c6f6106 /lib/iban.ml
parent29b26dcbc1404925bbf12cddd66f7fcd3c57cfe7 (diff)
downloadrdcapsis-95d50b25c990e8c945ce2507b16ff3c8b039d286.tar.gz
rdcapsis-95d50b25c990e8c945ce2507b16ff3c8b039d286.zip
OCaml
Diffstat (limited to 'lib/iban.ml')
-rw-r--r--lib/iban.ml87
1 files changed, 87 insertions, 0 deletions
diff --git a/lib/iban.ml b/lib/iban.ml
new file mode 100644
index 0000000..6e47e9d
--- /dev/null
+++ b/lib/iban.ml
@@ -0,0 +1,87 @@
1open Core
2open Option.Let_syntax
3
4type t = string
5
6(* Modulo-97 arithmetic. Prevents us from having to use Zarith here. *)
7module M97 : sig
8 type t
9
10 val of_int : int -> t
11 val lt : t -> t -> bool
12 val equal : t -> t -> bool
13 val ( * ) : t -> t -> t
14 val ( + ) : t -> t -> t
15 val ( ~$ ) : int -> t
16end = struct
17 type t = int
18
19 let of_int x = x % 97
20 let equal = Int.( = )
21 let lt = Int.( < )
22 let ( * ) x y = x * y % 97
23 let ( + ) x y = (x + y) % 97
24 let ( ~$ ) = of_int
25end
26
27let m97_of_alnum c =
28 let v = Char.to_int c in
29 if Char.is_digit c then Some (M97.of_int (v - Char.to_int '0'))
30 else if Char.is_alpha c then
31 if Char.is_lowercase c then Some (M97.of_int (v - Char.to_int 'a' + 10))
32 else Some (M97.of_int (v - Char.to_int 'A' + 10))
33 else None
34
35let m97_of_digit c =
36 match m97_of_alnum c with Some v when M97.(lt v ~$10) -> Some v | _ -> None
37
38let m97_of_alpha c =
39 match m97_of_alnum c with
40 | Some v when not M97.(lt v ~$10) -> Some v
41 | _ -> None
42
43let string_fold_option ~(init : 'a) ~(f : 'a -> char -> 'a option) s =
44 let rec go i (acc : 'a) : 'a option =
45 if i >= String.length s then Some acc
46 else Option.(f acc (String.unsafe_get s i) >>= go (i + 1))
47 in
48 go 0 init
49
50let m97_of_iban s =
51 string_fold_option s ~init:`In_country1 ~f:(fun st c ->
52 match st with
53 | `In_country1 ->
54 let%map co1 = m97_of_alpha c in
55 `In_country2 co1
56 | `In_country2 co1 ->
57 let%map co2 = m97_of_alpha c in
58 `In_check1 M97.((co1 * ~$100) + co2)
59 | `In_check1 co ->
60 let%map ch1 = m97_of_digit c in
61 `In_check2 (co, ch1)
62 | `In_check2 (co, ch1) ->
63 let%map ch2 = m97_of_digit c in
64 `In_bban M97.(co, (ch1 * ~$10) + ch2, ~$0)
65 | `In_bban (co, ch, bban) ->
66 let%map v = m97_of_alnum c in
67 let bban' =
68 M97.(if lt v ~$10 then (bban * ~$10) + v else (bban * ~$100) + v)
69 in
70 `In_bban (co, ch, bban'))
71 |> function
72 | Some (`In_bban (co, ch, bban)) ->
73 Some M97.((bban * ~$1000000) + (co * ~$100) + ch)
74 | _ -> None
75
76let check_iban s =
77 String.length s <= 34 && Option.exists (m97_of_iban s) ~f:M97.(equal ~$1)
78
79let make s : t option = if check_iban s then Some s else None
80let to_string = Fn.id
81
82let of_string s =
83 match make s with
84 | Some iban -> iban
85 | None -> Printf.failwithf "Iban.of_string: %S" s ()
86
87let equal = String.equal