diff options
author | Rutger Broekhoff | 2025-08-25 19:48:19 +0200 |
---|---|---|
committer | Rutger Broekhoff | 2025-08-25 19:48:19 +0200 |
commit | 95d50b25c990e8c945ce2507b16ff3c8b039d286 (patch) | |
tree | c1ff4c7f9601c6980eed1a7235ba336c5c6f6106 /lib/iban.ml | |
parent | 29b26dcbc1404925bbf12cddd66f7fcd3c57cfe7 (diff) | |
download | rdcapsis-95d50b25c990e8c945ce2507b16ff3c8b039d286.tar.gz rdcapsis-95d50b25c990e8c945ce2507b16ff3c8b039d286.zip |
OCaml
Diffstat (limited to 'lib/iban.ml')
-rw-r--r-- | lib/iban.ml | 87 |
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 @@ | |||
1 | open Core | ||
2 | open Option.Let_syntax | ||
3 | |||
4 | type t = string | ||
5 | |||
6 | (* Modulo-97 arithmetic. Prevents us from having to use Zarith here. *) | ||
7 | module 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 | ||
16 | end = 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 | ||
25 | end | ||
26 | |||
27 | let 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 | |||
35 | let m97_of_digit c = | ||
36 | match m97_of_alnum c with Some v when M97.(lt v ~$10) -> Some v | _ -> None | ||
37 | |||
38 | let 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 | |||
43 | let 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 | |||
50 | let 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 | |||
76 | let check_iban s = | ||
77 | String.length s <= 34 && Option.exists (m97_of_iban s) ~f:M97.(equal ~$1) | ||
78 | |||
79 | let make s : t option = if check_iban s then Some s else None | ||
80 | let to_string = Fn.id | ||
81 | |||
82 | let of_string s = | ||
83 | match make s with | ||
84 | | Some iban -> iban | ||
85 | | None -> Printf.failwithf "Iban.of_string: %S" s () | ||
86 | |||
87 | let equal = String.equal | ||