Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ on:
jobs:
fmt:
name: Format
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ocaml/setup-ocaml@v2
with:
ocaml-compiler: ocaml-base-compiler.4.14.1
dune-cache: true

- name: Install ocamlformat
run: make install-ocamlformat
Expand All @@ -32,7 +33,7 @@ jobs:
matrix:
os:
- macos-latest
- ubuntu-latest
- ubuntu-22.04
ocaml-compiler:
- ocaml-base-compiler.4.14.1
steps:
Expand All @@ -42,6 +43,7 @@ jobs:
uses: ocaml/setup-ocaml@v2
with:
ocaml-compiler: ${{ matrix.ocaml-compiler }}
dune-cache: true

- name: Install opam dependencies
run: opam install --deps-only --with-test .
Expand Down
49 changes: 49 additions & 0 deletions lib/ar/cbu.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
open Tools

exception Invalid_length
exception Invalid_format
exception Invalid_checksum

let compact number = Utils.clean number " -" |> String.trim

let calc_check_digit number =
let weights = [| 3; 1; 7; 9 |] in
let len = String.length number in
let rec sum i acc =
if i < len then
let w = weights.(i mod 4) in
let n = int_of_char number.[len - 1 - i] - int_of_char '0' in
sum (i + 1) (acc + (w * n))
else acc
in
let check = (10 - (sum 0 0 mod 10)) mod 10 in
string_of_int check

let validate number =
let cleaned = compact number in

(* Check for non-digits in cleaned string *)
if not (String.for_all (fun c -> c >= '0' && c <= '9') cleaned) then
raise Invalid_format;

(* Length check after format validation *)
if String.length cleaned <> 22 then raise Invalid_length;

let first_part = String.sub cleaned 0 7 in
let second_part = String.sub cleaned 8 13 in
let check1 = calc_check_digit first_part in
let check2 = calc_check_digit second_part in
if String.get cleaned 7 <> String.get check1 0 then raise Invalid_checksum
else if String.get cleaned 21 <> String.get check2 0 then
raise Invalid_checksum
else cleaned

let is_valid number =
try
let _ = validate number in
true
with Invalid_format | Invalid_length | Invalid_checksum -> false

let format number =
let number = compact number in
String.sub number 0 8 ^ " " ^ String.sub number 8 14
40 changes: 40 additions & 0 deletions lib/ar/cbu.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
(*
CBU (Clave Bancaria Uniforme, Argentine bank account number).

CBU it s a code of the Banks of Argentina to identify customer accounts. The
number consists of 22 digits and consists of a 3 digit bank identifier,
followed by a 4 digit branch identifier, a check digit, a 13 digit account
identifier and another check digit.

More information:

* https://es.wikipedia.org/wiki/Clave_Bancaria_Uniforme
*)

exception Invalid_length
(** Exception raised when the CBU number has an invalid length. *)

exception Invalid_format
(** Exception raised when the CBU number has an invalid format. *)

exception Invalid_checksum
(** Exception raised when the CBU number has an invalid checksum. *)

val validate : string -> string
(** Check if the number is a valid CBU. Returns the normalized number if valid.
@raise Invalid_length if the number length is not 22
@raise Invalid_format if the number contains non-digit characters
@raise Invalid_checksum if either check digit is invalid *)

val is_valid : string -> bool
(** Check if the number is a valid CBU. Returns true if valid, false otherwise. *)

val format : string -> string
(** Reformat the number to the standard presentation format with spaces. *)

val compact : string -> string
(** [compact number] removes spaces and dashes from the number. *)

val calc_check_digit : string -> string
(** [calc_check_digit number] calculates the check digit for the given number sequence
using the CBU algorithm. *)
4 changes: 4 additions & 0 deletions lib/ar/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(library
(name ar)
(public_name stdnum.ar)
(libraries stdnum.tools))
1 change: 1 addition & 0 deletions lib/tools/dune
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
(library
(name tools)
(public_name stdnum.tools)
(libraries str))
3 changes: 3 additions & 0 deletions test/ar/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(test
(name test_cbu)
(libraries alcotest stdnum.tools stdnum.ar))
146 changes: 146 additions & 0 deletions test/ar/test_cbu.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
open Ar.Cbu
open Alcotest

let test_valid_numbers () =
let numbers =
[
"0070999020000065706080"
; "0110433630043313857683"
; "0140339601630201381276"
; "0140023601506802625874"
; "0440064640000142941092"
; "0720146820000001062340"
; "0720168020000001183236"
; "0720380888000035533968"
; "0070034420000002310035"
; "0070085620000002598406"
; "0070089420000002991793"
; "0070090020000004146504"
; "0070109530004141775453"
; "0070114920000004100700"
; "0070274620000003448717"
; "0070999020000057705860"
; "0110097630009704213797"
; "0110102320010200444955"
; "0110106130010603111097"
; "0110106130010604601847"
; "0110125220012510923535"
; "0110130620013014594573"
; "0110175730017523189801"
; "0110204030020409626051"
; "0110216320021610025999"
; "0110230930023001323933"
; "0110230930023008918451"
; "0110283520028310814652"
; "0110363020036300101822"
; "0110377720037700120402"
; "0110385220038500036492"
; "0110409120040921180719"
; "0110424420042410570553"
; "0110454130045407688379"
; "0110477020047731297428"
; "0110508720050800019135"
; "0110521620052100223696"
; "0110551320055100112719"
; "0140313601697100515896"
; "0140313601697100557414"
; "0140339601630201381276"
; "0140351801684605023087"
; "0140352501684700733410"
; "0140352503684700819149"
; "0140369303631000285682"
; "0140391403672850026131"
; "0140410801680000361629"
; "0140417701630000088992"
; "0140444301650700088379"
; "0140476401626402048153"
; "0150501602000120967405"
; "0168888100008274410158"
; "0168888100000641080265"
; "0170074920000030293449"
; "0170334220000030367766"
; "0200306901000040010097"
; "0200348901000000334779"
; "0200398411000030044362"
; "0200405501000000213951"
; "0200451211000030033962"
; "0200915901000000274233"
; "0340056200560007577005"
; "0720000720000001681136"
; "0720079388000035942322"
; "0720297320000000081418"
; "0720402320000002633754"
; "0930301810100000992800"
; "0930301810100001043132"
; "0930310010100014278400"
; "0930324720100053299139"
; "0930324720100055211111"
; "0940099324001313220028"
; "1500006000005660447200"
; "1500087900051332075196"
; "1910119655011901084646"
; "1910104255110401549353"
; "1910126455012600786400"
; "1910186855018601143246"
; "1910369755036901130632"
; "2850345330000000781858"
; "2850353830094127564171"
; "2850376730000059833142"
; "2850400530094105352671"
; "2850536730094125514871"
; "2850590940090418135201"
; "2850729540000001576069"
; "2850732530000002707016"
; "2850734940094696942458"
; "2850760830094054972021"
; "2850882330094054578991"
; "3110003611000000537014"
; "3110013511000600125046"
; "3300542115420000740012"
; "3300551315510001836040"
; "3860002703000000438381"
; "3860011901000020526675"
; "3860060703000013990500"
; "5729195067928761667584"
; "7362966507842824472644"
; "9498175528566296510521"
]
in
List.iter
(fun n -> check bool (n ^ " should be valid") true (is_valid n))
numbers

let test_invalid_length () =
Alcotest.check_raises "should raise Invalid_length" Invalid_length (fun () ->
ignore (validate "285059094009041"))

let test_invalid_format () =
check_raises "should raise Invalid_format" Invalid_format (fun () ->
ignore (validate "A850590940090418135201"))

let test_valid_number () =
check string "should validate correct number" "0940099324001313220028"
(validate "0940099324001313220028")

let test_invalid_first_part () =
check_raises "should raise Invalid_checksum" Invalid_checksum (fun () ->
ignore (validate "1940099324001313220028"))

let test_invalid_second_part () =
check_raises "should raise Invalid_checksum" Invalid_checksum (fun () ->
ignore (validate "0940099324001313220038"))

let test_cases =
[
( "CBU validation"
, [
test_case "valid numbers" `Quick test_valid_numbers
; test_case "invalid length" `Quick test_invalid_length
; test_case "invalid format" `Quick test_invalid_format
; test_case "valid number" `Quick test_valid_number
; test_case "invalid first part" `Quick test_invalid_first_part
; test_case "invalid second part" `Quick test_invalid_second_part
] )
]

let () = run "CBU" test_cases
Loading