diff --git a/decimal.opam b/decimal.opam index b8b6e16..67f43c4 100644 --- a/decimal.opam +++ b/decimal.opam @@ -13,6 +13,7 @@ doc: "https://yawaramin.github.io/ocaml-decimal/api" bug-reports: "https://github.com/yawaramin/ocaml-decimal/issues" depends: [ "dune" {>= "2.7"} + "alcotest" {>= "1.5.0" & < "2.0.0" & with-test} "angstrom" {>= "0.15.0" & < "1.0.0" & with-test} "ocaml" {>= "4.08.0"} "zarith" {>= "1.10" & < "2.0.0"} diff --git a/dune-project b/dune-project index 89f2f1f..383f8fa 100644 --- a/dune-project +++ b/dune-project @@ -1,7 +1,7 @@ (lang dune 2.7) (name decimal) -(version v0.1.1) +(version v0.3.0) (generate_opam_files true) (license PSF-2.0) (authors "Yawar Amin ") @@ -15,6 +15,7 @@ the Python decimal module.") (documentation "https://yawaramin.github.io/ocaml-decimal/api") (depends + (alcotest (and (>= 1.5.0) (< 2.0.0) :with-test)) (angstrom (and (>= 0.15.0) (< 1.0.0) :with-test)) (ocaml (>= 4.08.0)) (zarith (and (>= 1.10) (< 2.0.0))))) diff --git a/lib/decimal.ml b/lib/decimal.ml index 6c38acb..221a53d 100644 --- a/lib/decimal.ml +++ b/lib/decimal.ml @@ -430,6 +430,22 @@ let of_float ?(context= !Context.default) value = | _ -> Context.raise ~msg:(Sign.to_string sign ^ str) Conversion_syntax context +let of_yojson = function + | `Int i -> + Ok (of_int i) + | `Float f -> + begin match of_float f with + | t -> Ok t + | exception Invalid_argument msg -> Error msg + end + | `String s -> + begin match of_string s with + | t -> Ok t + | exception Invalid_argument msg -> Error msg + end + | _ -> + Error "of_yojson: invalid argument" + let to_bool = function Finite { coef = "0"; _ } -> false | _ -> true let to_string ?(eng=false) ?(context= !Context.default) = function @@ -479,6 +495,8 @@ let to_string ?(eng=false) ?(context= !Context.default) = function in Sign.to_string sign ^ intpart ^ fracpart ^ exp +let to_yojson t = `String (to_string t) + let pp f t = t |> to_string |> Format.pp_print_string f let z10 = Calc.z10 diff --git a/lib/decimal.mli b/lib/decimal.mli index d747aac..efbc230 100644 --- a/lib/decimal.mli +++ b/lib/decimal.mli @@ -306,6 +306,17 @@ val of_bigint : Z.t -> t val of_int : int -> t val of_string : ?context:Context.t -> string -> t +val of_yojson : + [> `Int of int | `Float of float | `String of string] -> (t, string) result +(** [of_yojson json] is the result of parsing a JSON value into a decimal: + + - integer is parsed + - float is parsed with the usual caveat about float imprecision + - string is parsed + - anything else fails to parse + + @since 0.3.0 *) + val of_float : ?context:Context.t -> float -> t [@@alert lossy "Suffers from floating-point precision loss. Other constructors should be preferred."] (** [of_float ?context float] is the decimal representation of the [float]. This @@ -318,6 +329,13 @@ val to_bigint : t -> Z.t val to_bool : t -> bool val to_rational : t -> Q.t val to_string : ?eng:bool -> ?context:Context.t -> t -> string + +val to_yojson : t -> [> `String of string] +(** [to_yojson t] is the JSON representation of decimal value [t]. Note that it + is encoded as a string to avoid losing precision. + + @since 0.3.0 *) + val pp : Format.formatter -> t -> unit val to_tuple : t -> int * string * int diff --git a/test/decimal_test.ml b/test/decimal_test.ml index 4545c0d..5653ff9 100644 --- a/test/decimal_test.ml +++ b/test/decimal_test.ml @@ -196,5 +196,7 @@ let () = "data/remainder.decTest"; "data/subtract.decTest"; ]; + print_endline ""; + Json.test (); print_endline "\nOK." diff --git a/test/dune b/test/dune index a8f1e22..b4b1d13 100644 --- a/test/dune +++ b/test/dune @@ -1,4 +1,4 @@ (test (name decimal_test) (deps (glob_files data/*.decTest)) - (libraries angstrom decimal)) + (libraries alcotest angstrom decimal)) diff --git a/test/json.ml b/test/json.ml new file mode 100644 index 0000000..fced357 --- /dev/null +++ b/test/json.ml @@ -0,0 +1,62 @@ +open Alcotest + +let decimal = (module Decimal : TESTABLE with type t = Decimal.t) + +let test () = + run "Decimal" [ + "of_yojson", [ + test_case "int" `Quick begin fun () -> + `Int 0 + |> Decimal.of_yojson + |> Result.get_ok + |> check decimal "0" Decimal.zero + end; + + test_case "int" `Quick begin fun () -> + `Int ~-1 + |> Decimal.of_yojson + |> Result.get_ok + |> check decimal "-1" (Decimal.of_int ~-1) + end; + + test_case "float" `Quick begin[@alert "-lossy"] fun () -> + `Float 1. + |> Decimal.of_yojson + |> Result.get_ok + |> check decimal "1.0" (Decimal.of_float 1.) + end; + + test_case "float" `Quick begin[@alert "-lossy"] fun () -> + let nan = "NaN" in + `Float Float.nan + |> Decimal.of_yojson + |> Result.get_ok + |> Decimal.to_string + |> check string nan nan + end; + + test_case "string" `Quick begin fun () -> + let pi = "3.14159" in + `String pi + |> Decimal.of_yojson + |> Result.get_ok + |> check decimal pi (Decimal.of_string pi) + end; + + test_case "other" `Quick begin fun () -> + `Null + |> Decimal.of_yojson + |> Result.get_error + |> check string "null" "of_yojson: invalid argument" + end; + ]; + + "to_yojson", [ + test_case "pi" `Quick begin fun () -> + let pi = "3.14159" in + match pi |> Decimal.of_string |> Decimal.to_yojson with + | `String s -> check string pi pi s + | _ -> fail "to_yojson must always produce a string" + end; + ]; + ]