Skip to content

Commit

Permalink
Adds float to decimal decoding option
Browse files Browse the repository at this point in the history
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 <tiagonbotelho@gmail.com>
  • Loading branch information
michalmuskala and tiagonbotelho committed Dec 21, 2021
1 parent 40d9c51 commit 1ffe009
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 14 deletions.
46 changes: 34 additions & 12 deletions lib/decoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand All @@ -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 ->
Expand Down Expand Up @@ -160,7 +186,8 @@ defmodule Jason.Decoder do
end
defp number_frac_cont(<<rest::bits>>, 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

Expand Down Expand Up @@ -190,7 +217,8 @@ defmodule Jason.Decoder do
end
defp number_exp_cont(<<rest::bits>>, 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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions lib/jason.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
13 changes: 13 additions & 0 deletions test/decode_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -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 (",")|
Expand Down

0 comments on commit 1ffe009

Please sign in to comment.