From 1ffe009b6d9117de05b61e1fbf9ecc6744052588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muska=C5=82a?= Date: Tue, 21 Dec 2021 13:57:44 +0100 Subject: [PATCH] Adds float to decimal decoding option This allows to decode floats to full-precision Decimal structs. This is useful for some protocols that aparently mis-treat JSON numbers as full-precision. I could not measure a slowdown because of this addition with the default options. Closes #115, #81 Co-authored-by: Tiago Botelho --- lib/decoder.ex | 46 ++++++++++++++++++++++++++++++++------------ lib/jason.ex | 11 +++++++++-- test/decode_test.exs | 13 +++++++++++++ 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/lib/decoder.ex b/lib/decoder.ex index 71c59ed..7511b27 100644 --- a/lib/decoder.ex +++ b/lib/decoder.ex @@ -43,13 +43,15 @@ defmodule Jason.Decoder do @key 2 @object 3 - defrecordp :decode, [keys: nil, strings: nil] + defrecordp :decode, [keys: nil, strings: nil, floats: nil] def parse(data, opts) when is_binary(data) do key_decode = key_decode_function(opts) string_decode = string_decode_function(opts) + float_decode = float_decode_function(opts) + decode = decode(keys: key_decode, strings: string_decode, floats: float_decode) try do - value(data, data, 0, [@terminate], decode(keys: key_decode, strings: string_decode)) + value(data, data, 0, [@terminate], decode) catch {:position, position} -> {:error, %DecodeError{position: position, data: data}} @@ -69,6 +71,30 @@ defmodule Jason.Decoder do defp string_decode_function(%{strings: :copy}), do: &:binary.copy/1 defp string_decode_function(%{strings: :reference}), do: &(&1) + defp float_decode_function(%{floats: :native}) do + fn string, token, skip -> + try do + :erlang.binary_to_float(string) + catch + :error, :badarg -> + token_error(token, skip) + end + end + end + + defp float_decode_function(%{floats: :decimals}) do + fn string, token, skip -> + # silence xref warning + decimal = Decimal + try do + decimal.new(string) + rescue + Decimal.Error -> + token_error(token, skip) + end + end + end + defp value(data, original, skip, stack, decode) do bytecase data do _ in '\s\n\t\r', rest -> @@ -160,7 +186,8 @@ defmodule Jason.Decoder do end defp number_frac_cont(<>, original, skip, stack, decode, len) do token = binary_part(original, skip, len) - float = try_parse_float(token, token, skip) + decode(floats: float_decode) = decode + float = float_decode.(token, token, skip) continue(rest, original, skip + len, stack, decode, float) end @@ -190,7 +217,8 @@ defmodule Jason.Decoder do end defp number_exp_cont(<>, original, skip, stack, decode, len) do token = binary_part(original, skip, len) - float = try_parse_float(token, token, skip) + decode(floats: float_decode) = decode + float = float_decode.(token, token, skip) continue(rest, original, skip + len, stack, decode, float) end @@ -225,7 +253,8 @@ defmodule Jason.Decoder do initial_skip = skip - prefix_size - 1 final_skip = skip + len token = binary_part(original, initial_skip, prefix_size + len + 1) - float = try_parse_float(string, token, initial_skip) + decode(floats: float_decode) = decode + float = float_decode.(string, token, initial_skip) continue(rest, original, final_skip, stack, decode, float) end @@ -610,13 +639,6 @@ defmodule Jason.Decoder do error(original, skip + 6) end - defp try_parse_float(string, token, skip) do - :erlang.binary_to_float(string) - catch - :error, :badarg -> - token_error(token, skip) - end - defp error(<<_rest::bits>>, _original, skip, _stack, _decode) do throw {:position, skip - 1} end diff --git a/lib/jason.ex b/lib/jason.ex index 42a8b43..a61ad1d 100644 --- a/lib/jason.ex +++ b/lib/jason.ex @@ -14,7 +14,9 @@ defmodule Jason do @type strings :: :reference | :copy - @type decode_opt :: {:keys, keys} | {:strings, strings} + @type floats :: :native | :decimals + + @type decode_opt :: {:keys, keys} | {:strings, strings} | {:floats, floats} @doc """ Parses a JSON value from `input` iodata. @@ -36,6 +38,11 @@ defmodule Jason do decoded data will be stored for a long time (in ets or some process) to avoid keeping the reference to the original data. + * `:floats` - controls how floats are decoded. Possible values are: + + * `:native` (default) - Native conversion from binary to float using `:erlang.binary_to_float/1`, + * `:decimals` - uses `Decimal.new/1` to parse the binary into a Decimal struct with arbitrary precision. + ## Decoding keys to atoms The `:atoms` option uses the `String.to_atom/1` call that can create atoms at runtime. @@ -223,6 +230,6 @@ defmodule Jason do end defp format_decode_opts(opts) do - Enum.into(opts, %{keys: :strings, strings: :reference}) + Enum.into(opts, %{keys: :strings, strings: :reference, floats: :native}) end end diff --git a/test/decode_test.exs b/test/decode_test.exs index 8a313f9..5397071 100644 --- a/test/decode_test.exs +++ b/test/decode_test.exs @@ -32,6 +32,7 @@ defmodule Jason.DecodeTest do assert parse!("-99.99e-99") == -99.99e-99 assert parse!("123456789.123456789e123") == 123456789.123456789e123 end + test "strings" do assert_fail_with ~s("), ~S|unexpected end of input at position 1| assert_fail_with ~s("\\"), ~S|unexpected end of input at position 3| @@ -111,6 +112,18 @@ defmodule Jason.DecodeTest do assert parse!(~s({"FOO": "bar"}), keys: &String.downcase/1) == %{"foo" => "bar"} end + test "parsing floats to decimals" do + assert parse!("0.1", floats: :decimals) == Decimal.new("0.1") + assert parse!("-0.1", floats: :decimals) == Decimal.new("-0.1") + assert parse!("1.0e0", floats: :decimals) == Decimal.new("1.0e0") + assert parse!("1.0e+0", floats: :decimals) == Decimal.new("1.0e+0") + assert parse!("0.1e1", floats: :decimals) == Decimal.new("0.1e1") + assert parse!("0.1e-1", floats: :decimals) == Decimal.new("0.1e-1") + + assert parse!("123456789.123456789e123", floats: :decimals) == + Decimal.new("123456789.123456789e123") + end + test "arrays" do assert_fail_with "[", ~S|unexpected end of input at position 1| assert_fail_with "[,", ~S|unexpected byte at position 1: 0x2C (",")|