From ae718618d9256b0db3c1b16e66ee12145ea0af4f Mon Sep 17 00:00:00 2001 From: swrup Date: Sun, 1 Mar 2026 00:35:59 +0100 Subject: [PATCH] --- src/amount.ml | 131 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 52 deletions(-) diff --git a/src/amount.ml b/src/amount.ml index 9b8b34ff..473fb4ec 100644 --- a/src/amount.ml +++ b/src/amount.ml @@ -14,67 +14,96 @@ type t = { fraction: Int32.t; } +let maximum_currency_length = 11 let value_upper_bound = Int64.of_float @@ Float.pow 2. 52. +let fraction_max_number_of_digits = 8 -(* TODO - the constraint is on the number of digits, - so this wrong if leading 0s - this depends on currency..? *) -let fraction_upper_bound = Int32.of_int 100_000_000 +(* TODO choose to either include or exclude upper limit for all bounds + need a -1 here *) +let fraction_upper_bound = + Int32.of_float @@ Float.pow 10. (Int.to_float fraction_max_number_of_digits) + +let check_currency s = + if not @@ String.for_all is_letter s then + Error "currency contains forbidden character" + else + let len = String.length s in + if len = 0 then Error "currency is empty string" + else if len > maximum_currency_length then + Fmt.error "currency is more than %d characters long" + maximum_currency_length + else Ok () + +let check_value v = + if Int64.unsigned_compare v value_upper_bound > 0 then + Error "value is greater than 2^52" + else Ok v + +let check_fraction v = + if Int32.unsigned_compare v fraction_upper_bound > 0 then + Fmt.error "fraction has more than %d digits" fraction_max_number_of_digits + else Ok v let make ~sign ~currency ~value ~fraction = - if value < Int64.zero then Error "value is negative" - else if fraction < Int32.zero then Error "fraction is negative" - else if value > value_upper_bound then Error "value is greater than 2^52-1" - else if fraction >= fraction_upper_bound then - Error "fraction has more than 8 decimal digits" - else Ok { sign; currency; value; fraction } + Ok { sign; currency; value; fraction } -module Parse = struct - open Angstrom +open Angstrom - let sign = - char '+' *> return (Some Sign_plus) - <|> char '-' *> return (Some Sign_minus) - <|> return None +let is_letter = function 'a' .. 'z' | 'A' .. 'Z' -> true | _ -> false +let is_digit = function '0' .. '9' -> true | _ -> false +let letters = take_while1 is_letter +let digits = take_while1 is_digit - let currency = - take_while1 (function 'a' .. 'z' | 'A' .. 'Z' -> true | _ -> false) - >>= fun s -> - match String.length s < 12 with - | false -> fail "currency is more than 11 characters" - | true -> return s +let sign = + choice + [ + char '+' *> return (Some Sign_plus); + char '-' *> return (Some Sign_minus); + return None; + ] - let int64 = - take_while1 (function '0' .. '9' -> true | _ -> false) - >>| Int64.of_string_opt - >>= function - | None -> fail "value is not a valid int64" - | Some n when n >= value_upper_bound -> fail "value is greater than 2^52-1" - | Some n -> return n +let currency_of_string s = + if not @@ String.for_all is_letter s then + Error "currency contains forbidden character" + else if String.length s > maximum_currency_length then + Fmt.error "currency is more than %d characters long" maximum_currency_length + else Ok (String.uppercase_ascii s) - let int32 = - take_while1 (function '0' .. '9' -> true | _ -> false) - >>| Int32.of_string_opt - >>= function - | None -> fail "fraction is not a valid int32" - | Some n when n >= fraction_upper_bound -> - fail "fraction is greater than 10^8-1" - | Some n -> return n +let value_of_string s = + if not @@ String.for_all is_digit s then + Error "value contains forbidden character" + else + match Int64.of_string_opt s with + | None -> Error "value is not a valid integer" + | Some v -> + if v > value_upper_bound then Error "value is greater than 2^52" + else if v < Int64.zero then Error "value is negative" + else Ok v - let amount = - lift4 - (fun sign currency value fraction -> - make ~sign ~currency ~value ~fraction) - sign currency - (char ':' *> int64) - (char '.' *> int32 <|> return 0_l) - <* end_of_input +let fraction_of_string s = + if not @@ String.for_all is_digit s then + Error "fraction contains forbidden character" + else if String.length s > fraction_max_number_of_digits then + Fmt.error "fraction has more than %d digits" fraction_max_number_of_digits + else + match Int32.of_string_opt s with + | None -> Error "fraction is not a valid integer" + | Some v -> if v < Int32.zero then Error "fraction is negative" else Ok v - let f s = parse_string ~consume:Consume.All amount s |> Result.join -end +let amount = + lift4 + (fun sign currency value fraction -> + let open Syntax in + let* currency = currency_of_string currency in + let* value = value_of_string value in + let* fraction = fraction_of_string fraction in + Ok { sign; currency; value; fraction }) + sign letters + (char ':' *> digits) + (option "" (char '.' *> digits)) + <* end_of_input -let of_string = Parse.f +let of_string s = parse_string ~consume:Consume.All amount s |> Result.join let pp = let open Fmt in @@ -92,9 +121,7 @@ let to_string = Fmt.str "%a" pp (* - *) let jsont = Jsont.of_of_string ~kind:"Amount" of_string ~enc:to_string - -(* byte length of currency string *) -let currency_len = 12 +let currency_length = 12 let pad_currency_string s = let len = String.length s in