Skip to content

Commit 1ffe009

Browse files
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 michalmuskala#115, michalmuskala#81 Co-authored-by: Tiago Botelho <[email protected]>
1 parent 40d9c51 commit 1ffe009

File tree

3 files changed

+56
-14
lines changed

3 files changed

+56
-14
lines changed

lib/decoder.ex

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ defmodule Jason.Decoder do
4343
@key 2
4444
@object 3
4545

46-
defrecordp :decode, [keys: nil, strings: nil]
46+
defrecordp :decode, [keys: nil, strings: nil, floats: nil]
4747

4848
def parse(data, opts) when is_binary(data) do
4949
key_decode = key_decode_function(opts)
5050
string_decode = string_decode_function(opts)
51+
float_decode = float_decode_function(opts)
52+
decode = decode(keys: key_decode, strings: string_decode, floats: float_decode)
5153
try do
52-
value(data, data, 0, [@terminate], decode(keys: key_decode, strings: string_decode))
54+
value(data, data, 0, [@terminate], decode)
5355
catch
5456
{:position, position} ->
5557
{:error, %DecodeError{position: position, data: data}}
@@ -69,6 +71,30 @@ defmodule Jason.Decoder do
6971
defp string_decode_function(%{strings: :copy}), do: &:binary.copy/1
7072
defp string_decode_function(%{strings: :reference}), do: &(&1)
7173

74+
defp float_decode_function(%{floats: :native}) do
75+
fn string, token, skip ->
76+
try do
77+
:erlang.binary_to_float(string)
78+
catch
79+
:error, :badarg ->
80+
token_error(token, skip)
81+
end
82+
end
83+
end
84+
85+
defp float_decode_function(%{floats: :decimals}) do
86+
fn string, token, skip ->
87+
# silence xref warning
88+
decimal = Decimal
89+
try do
90+
decimal.new(string)
91+
rescue
92+
Decimal.Error ->
93+
token_error(token, skip)
94+
end
95+
end
96+
end
97+
7298
defp value(data, original, skip, stack, decode) do
7399
bytecase data do
74100
_ in '\s\n\t\r', rest ->
@@ -160,7 +186,8 @@ defmodule Jason.Decoder do
160186
end
161187
defp number_frac_cont(<<rest::bits>>, original, skip, stack, decode, len) do
162188
token = binary_part(original, skip, len)
163-
float = try_parse_float(token, token, skip)
189+
decode(floats: float_decode) = decode
190+
float = float_decode.(token, token, skip)
164191
continue(rest, original, skip + len, stack, decode, float)
165192
end
166193

@@ -190,7 +217,8 @@ defmodule Jason.Decoder do
190217
end
191218
defp number_exp_cont(<<rest::bits>>, original, skip, stack, decode, len) do
192219
token = binary_part(original, skip, len)
193-
float = try_parse_float(token, token, skip)
220+
decode(floats: float_decode) = decode
221+
float = float_decode.(token, token, skip)
194222
continue(rest, original, skip + len, stack, decode, float)
195223
end
196224

@@ -225,7 +253,8 @@ defmodule Jason.Decoder do
225253
initial_skip = skip - prefix_size - 1
226254
final_skip = skip + len
227255
token = binary_part(original, initial_skip, prefix_size + len + 1)
228-
float = try_parse_float(string, token, initial_skip)
256+
decode(floats: float_decode) = decode
257+
float = float_decode.(string, token, initial_skip)
229258
continue(rest, original, final_skip, stack, decode, float)
230259
end
231260

@@ -610,13 +639,6 @@ defmodule Jason.Decoder do
610639
error(original, skip + 6)
611640
end
612641

613-
defp try_parse_float(string, token, skip) do
614-
:erlang.binary_to_float(string)
615-
catch
616-
:error, :badarg ->
617-
token_error(token, skip)
618-
end
619-
620642
defp error(<<_rest::bits>>, _original, skip, _stack, _decode) do
621643
throw {:position, skip - 1}
622644
end

lib/jason.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ defmodule Jason do
1414

1515
@type strings :: :reference | :copy
1616

17-
@type decode_opt :: {:keys, keys} | {:strings, strings}
17+
@type floats :: :native | :decimals
18+
19+
@type decode_opt :: {:keys, keys} | {:strings, strings} | {:floats, floats}
1820

1921
@doc """
2022
Parses a JSON value from `input` iodata.
@@ -36,6 +38,11 @@ defmodule Jason do
3638
decoded data will be stored for a long time (in ets or some process) to avoid keeping
3739
the reference to the original data.
3840
41+
* `:floats` - controls how floats are decoded. Possible values are:
42+
43+
* `:native` (default) - Native conversion from binary to float using `:erlang.binary_to_float/1`,
44+
* `:decimals` - uses `Decimal.new/1` to parse the binary into a Decimal struct with arbitrary precision.
45+
3946
## Decoding keys to atoms
4047
4148
The `:atoms` option uses the `String.to_atom/1` call that can create atoms at runtime.
@@ -223,6 +230,6 @@ defmodule Jason do
223230
end
224231

225232
defp format_decode_opts(opts) do
226-
Enum.into(opts, %{keys: :strings, strings: :reference})
233+
Enum.into(opts, %{keys: :strings, strings: :reference, floats: :native})
227234
end
228235
end

test/decode_test.exs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule Jason.DecodeTest do
3232
assert parse!("-99.99e-99") == -99.99e-99
3333
assert parse!("123456789.123456789e123") == 123456789.123456789e123
3434
end
35+
3536
test "strings" do
3637
assert_fail_with ~s("), ~S|unexpected end of input at position 1|
3738
assert_fail_with ~s("\\"), ~S|unexpected end of input at position 3|
@@ -111,6 +112,18 @@ defmodule Jason.DecodeTest do
111112
assert parse!(~s({"FOO": "bar"}), keys: &String.downcase/1) == %{"foo" => "bar"}
112113
end
113114

115+
test "parsing floats to decimals" do
116+
assert parse!("0.1", floats: :decimals) == Decimal.new("0.1")
117+
assert parse!("-0.1", floats: :decimals) == Decimal.new("-0.1")
118+
assert parse!("1.0e0", floats: :decimals) == Decimal.new("1.0e0")
119+
assert parse!("1.0e+0", floats: :decimals) == Decimal.new("1.0e+0")
120+
assert parse!("0.1e1", floats: :decimals) == Decimal.new("0.1e1")
121+
assert parse!("0.1e-1", floats: :decimals) == Decimal.new("0.1e-1")
122+
123+
assert parse!("123456789.123456789e123", floats: :decimals) ==
124+
Decimal.new("123456789.123456789e123")
125+
end
126+
114127
test "arrays" do
115128
assert_fail_with "[", ~S|unexpected end of input at position 1|
116129
assert_fail_with "[,", ~S|unexpected byte at position 1: 0x2C (",")|

0 commit comments

Comments
 (0)