Skip to content

Commit 5f5e217

Browse files
committed
CBU (Clave Bancaria Uniforme, Argentine bank account number)
1 parent 74972a7 commit 5f5e217

File tree

6 files changed

+235
-0
lines changed

6 files changed

+235
-0
lines changed

lib/ar/cbu.ml

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
open Tools
2+
3+
exception Invalid_length
4+
exception Invalid_format
5+
exception Invalid_checksum
6+
7+
let compact number = Utils.clean number " -" |> String.trim
8+
9+
let calc_check_digit number =
10+
let weights = [| 3; 1; 7; 9 |] in
11+
let len = String.length number in
12+
let rec sum i acc =
13+
if i < len then
14+
let w = weights.((len - 1 - i) mod 4) in
15+
let n = int_of_char number.[i] - int_of_char '0' in
16+
sum (i + 1) (acc + (w * n))
17+
else acc
18+
in
19+
string_of_int ((10 - sum 0 0) mod 10)
20+
21+
let validate number =
22+
let number = compact number in
23+
if String.length number <> 22 then raise Invalid_length
24+
else if not (Utils.is_digits number) then raise Invalid_format
25+
else if number.[7] <> (calc_check_digit (String.sub number 0 7)).[0] then
26+
raise Invalid_checksum
27+
else if
28+
number.[String.length number - 1]
29+
<> (calc_check_digit (String.sub number 8 (String.length number - 9))).[0]
30+
then raise Invalid_checksum
31+
else number
32+
33+
let is_valid number =
34+
try
35+
ignore (validate number);
36+
true
37+
with Invalid_format | Invalid_length | Invalid_checksum -> false
38+
39+
let format number =
40+
let number = compact number in
41+
String.sub number 0 8 ^ " " ^ String.sub number 8 14

lib/ar/cbu.mli

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
(*
2+
CBU (Clave Bancaria Uniforme, Argentine bank account number).
3+
4+
CBU it s a code of the Banks of Argentina to identify customer accounts. The
5+
number consists of 22 digits and consists of a 3 digit bank identifier,
6+
followed by a 4 digit branch identifier, a check digit, a 13 digit account
7+
identifier and another check digit.
8+
9+
More information:
10+
11+
* https://es.wikipedia.org/wiki/Clave_Bancaria_Uniforme
12+
*)
13+
14+
exception Invalid_length
15+
(** Exception raised when the CBU number has an invalid length. *)
16+
17+
exception Invalid_format
18+
(** Exception raised when the CBU number has an invalid format. *)
19+
20+
exception Invalid_checksum
21+
(** Exception raised when the CBU number has an invalid checksum. *)
22+
23+
val validate : string -> string
24+
(** Check if the number is a valid CBU. Returns the normalized number if valid.
25+
@raise Invalid_length if the number length is not 22
26+
@raise Invalid_format if the number contains non-digit characters
27+
@raise Invalid_checksum if either check digit is invalid *)
28+
29+
val is_valid : string -> bool
30+
(** Check if the number is a valid CBU. Returns true if valid, false otherwise. *)
31+
32+
val format : string -> string
33+
(** Reformat the number to the standard presentation format with spaces. *)
34+
35+
val compact : string -> string
36+
(** [compact number] removes spaces and dashes from the number. *)
37+
38+
val calc_check_digit : string -> string
39+
(** [calc_check_digit number] calculates the check digit for the given number sequence
40+
using the CBU algorithm. *)

lib/ar/dune

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
(library
2+
(name ar)
3+
(public_name stdnum.ar)
4+
(libraries stdnum.tools))

lib/tools/dune

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
(library
22
(name tools)
3+
(public_name stdnum.tools)
34
(libraries str))

test/ar/dune

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(test
2+
(name test_cbu)
3+
(libraries alcotest stdnum.tools stdnum.ar))

test/ar/test_cbu.ml

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
open Ar.Cbu
2+
open Alcotest
3+
4+
let test_valid_numbers () =
5+
let numbers =
6+
[
7+
"0070999020000065706080"
8+
; "0110433630043313857683"
9+
; "0140339601630201381276"
10+
; "0140023601506802625874"
11+
; "0440064640000142941092"
12+
; "0720146820000001062340"
13+
; "0720168020000001183236"
14+
; "0720380888000035533968"
15+
; "0070034420000002310035"
16+
; "0070085620000002598406"
17+
; "0070089420000002991793"
18+
; "0070090020000004146504"
19+
; "0070109530004141775453"
20+
; "0070114920000004100700"
21+
; "0070274620000003448717"
22+
; "0070999020000057705860"
23+
; "0110097630009704213797"
24+
; "0110102320010200444955"
25+
; "0110106130010603111097"
26+
; "0110106130010604601847"
27+
; "0110125220012510923535"
28+
; "0110130620013014594573"
29+
; "0110175730017523189801"
30+
; "0110204030020409626051"
31+
; "0110216320021610025999"
32+
; "0110230930023001323933"
33+
; "0110230930023008918451"
34+
; "0110283520028310814652"
35+
; "0110363020036300101822"
36+
; "0110377720037700120402"
37+
; "0110385220038500036492"
38+
; "0110409120040921180719"
39+
; "0110424420042410570553"
40+
; "0110454130045407688379"
41+
; "0110477020047731297428"
42+
; "0110508720050800019135"
43+
; "0110521620052100223696"
44+
; "0110551320055100112719"
45+
; "0140313601697100515896"
46+
; "0140313601697100557414"
47+
; "0140339601630201381276"
48+
; "0140351801684605023087"
49+
; "0140352501684700733410"
50+
; "0140352503684700819149"
51+
; "0140369303631000285682"
52+
; "0140391403672850026131"
53+
; "0140410801680000361629"
54+
; "0140417701630000088992"
55+
; "0140444301650700088379"
56+
; "0140476401626402048153"
57+
; "0150501602000120967405"
58+
; "0168888100008274410158"
59+
; "0168888100000641080265"
60+
; "0170074920000030293449"
61+
; "0170334220000030367766"
62+
; "0200306901000040010097"
63+
; "0200348901000000334779"
64+
; "0200398411000030044362"
65+
; "0200405501000000213951"
66+
; "0200451211000030033962"
67+
; "0200915901000000274233"
68+
; "0340056200560007577005"
69+
; "0720000720000001681136"
70+
; "0720079388000035942322"
71+
; "0720297320000000081418"
72+
; "0720402320000002633754"
73+
; "0930301810100000992800"
74+
; "0930301810100001043132"
75+
; "0930310010100014278400"
76+
; "0930324720100053299139"
77+
; "0930324720100055211111"
78+
; "0940099324001313220028"
79+
; "1500006000005660447200"
80+
; "1500087900051332075196"
81+
; "1910119655011901084646"
82+
; "1910104255110401549353"
83+
; "1910126455012600786400"
84+
; "1910186855018601143246"
85+
; "1910369755036901130632"
86+
; "2850345330000000781858"
87+
; "2850353830094127564171"
88+
; "2850376730000059833142"
89+
; "2850400530094105352671"
90+
; "2850536730094125514871"
91+
; "2850590940090418135201"
92+
; "2850729540000001576069"
93+
; "2850732530000002707016"
94+
; "2850734940094696942458"
95+
; "2850760830094054972021"
96+
; "2850882330094054578991"
97+
; "3110003611000000537014"
98+
; "3110013511000600125046"
99+
; "3300542115420000740012"
100+
; "3300551315510001836040"
101+
; "3860002703000000438381"
102+
; "3860011901000020526675"
103+
; "3860060703000013990500"
104+
; "5729195067928761667584"
105+
; "7362966507842824472644"
106+
; "9498175528566296510521"
107+
]
108+
in
109+
List.iter
110+
(fun n -> check bool (n ^ " should be valid") true (is_valid n))
111+
numbers
112+
113+
let test_invalid_length () =
114+
Alcotest.check_raises "should raise Invalid_length" Invalid_length (fun () ->
115+
ignore (validate "285059094009041"))
116+
117+
let test_invalid_format () =
118+
check_raises "should raise Invalid_format" Invalid_format (fun () ->
119+
ignore "A850590940090418135201")
120+
121+
let test_valid_number () =
122+
check string "should validate correct number" "0940099324001313220028"
123+
(validate "0940099324001313220028")
124+
125+
let test_invalid_first_part () =
126+
check_raises "should raise Invalid_checksum" Invalid_checksum (fun () ->
127+
ignore "1940099324001313220028")
128+
129+
let test_invalid_second_part () =
130+
check_raises "should raise Invalid_checksum" Invalid_checksum (fun () ->
131+
ignore "0940099324001313220038")
132+
133+
let test_cases =
134+
[
135+
( "CBU validation"
136+
, [
137+
test_case "valid numbers" `Quick test_valid_numbers
138+
; test_case "invalid length" `Quick test_invalid_length
139+
; test_case "invalid format" `Quick test_invalid_format
140+
; test_case "valid number" `Quick test_valid_number
141+
; test_case "invalid first part" `Quick test_invalid_first_part
142+
; test_case "invalid second part" `Quick test_invalid_second_part
143+
] )
144+
]
145+
146+
let () = run "CBU" test_cases

0 commit comments

Comments
 (0)