(* TODO have safe amount arithmetic make type private *) (* Amounts of currency, serialized as `:.` Fixed-precision numbers with 8 decimal places. - must be at most 11 characters long only consist of ASCII letters (a-zA-Z). - integer part of may be at most 2^52. - fractional part of may contain at most 8 decimal digits. Prefixed with '+' or '-' in certain contexts. When no sign is present, the amount is assumed to be positive. *) type t = { sign: [ `Plus | `Minus ] option; currency: [ `Eur ]; value: Int64.t; fraction: Int64.t; } let ( let* ) o f = match o with Ok v -> f v | Error _ as e -> e let make ~sign ~currency ~value ~fraction = let value_upper_bound = Z.(pow (of_int 2) 52) |> Z.to_int64 in let fraction_upper_bound = Int64.of_int 99_999_999 in let* () = if value < Int64.zero then Error "value is negative" else Ok () in let* () = if fraction < Int64.zero then Error "fraction is negative" else Ok () in let* () = if value > value_upper_bound then Error "value is greater than 2^52" else Ok () in let* () = if fraction > fraction_upper_bound then Error "fraction have more than 8 decimal digits" else Ok () in Ok { sign; currency; value; fraction } let to_string = let pp = let open Fmt in let pp_sign ppf = function | `Plus -> char ppf '+' | `Minus -> char ppf '-' in let pp_currency ppf = function `Eur -> string ppf "EUR" in fun ppf { sign; currency; value; fraction } -> (* TODO amount use config *) pf ppf "%a%a:%Ld.%02Ld" (Fmt.option pp_sign) sign pp_currency currency value fraction in Fmt.str "%a" pp let of_string = let open Angstrom in let parse_sign = choice [ char '+' *> return (Some `Plus); char '-' *> return (Some `Minus); return None; ] in let parse_currency = string "EUR" *> return `Eur in let parse_int = take_while1 (function '0' .. '9' -> true | _ -> false) >>| Int64.of_string_opt >>= function | None -> fail "invalid integer" | Some n -> return n in let parse_t = lift4 (fun sign currency value fraction -> make ~sign ~currency ~value ~fraction) parse_sign parse_currency (char ':' *> parse_int) (char '.' *> parse_int) in fun s -> parse_string ~consume:Consume.All parse_t s |> Result.join