Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added float_as_string option to prevent losing of precision of float … #159

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ option() = dirty_strings
| return_tail
| uescape
| unescaped_jsonp
| float_as_string

strict_option() = comments
| trailing_commas
Expand Down Expand Up @@ -415,6 +416,13 @@ additional options beyond these. see
these codepoints are escaped (to `\u2028` and `\u2029`, respectively) to
retain compatibility. this option simply removes that escaping

- `float_as_string`

the precision of float values in jsons are not restricted by the IEEE 754 standard.
in some cases it is not allowed to lose precision during the decoding of a json structure,
this option decodes the float values as strings. this way it possible to handle these values with
solutions that preserve precision, like the decimal library.


## exports ##

Expand Down
47 changes: 47 additions & 0 deletions src/jsx.erl
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ test_cases() ->
++ floats()
++ compound_object().

float_as_string_test_cases() ->
string_floats().

%% segregate these so we can skip them in `jsx_to_term`
special_test_cases() -> special_objects() ++ special_array().

Expand Down Expand Up @@ -303,6 +306,34 @@ naked_floats() ->
|| X <- Raw ++ [ -1 * Y || Y <- Raw ]
].

floats_as_string() ->
Raw = [
"0.0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9",
"1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9",
"1234567890.0987654321",
"0.0e0",
"1234567890.0987654321e16",
"0.1e0", "0.1e1", "0.1e2", "0.1e4", "0.1e8", "0.1e16", "0.1e308",
"1.0e0", "1.0e1", "1.0e2", "1.0e4", "1.0e8", "1.0e16", "1.0e308",
"2.2250738585072014e-308", %% min normalized float
"1.7976931348623157e308", %% max normalized float
"5.0e-324", %% min denormalized float
"2.225073858507201e-308" %% max denormalized float
],
[
{
X,
list_to_binary(X),
X,
[{string, list_to_binary(X)}]
}
|| X <- Raw
].

string_floats() ->
floats_as_string()
++ [ wrap_with_array(Test) || Test <- floats_as_string() ]
++ [ wrap_with_object(Test) || Test <- floats_as_string() ].

floats() ->
naked_floats()
Expand Down Expand Up @@ -425,6 +456,14 @@ incremental_decode(JSON) ->
),
Final(end_stream).

incremental_decode_float_as_string(JSON) ->
Final = lists:foldl(
fun(Byte, Decoder) -> {incomplete, F} = Decoder(Byte), F end,
decoder(jsx, [], [stream, float_as_string]),
json_to_bytes(JSON)
),
Final(end_stream).


incremental_parse(Events) ->
Final = lists:foldl(
Expand Down Expand Up @@ -453,6 +492,14 @@ decode_test_() ->
|| {Title, JSON, _, Events} <- Data
].

decode_float_as_string_test_() ->
Data = float_as_string_test_cases(),
[{Title, ?_assertEqual(Events ++ [end_json], (decoder(?MODULE, [], [float_as_string]))(JSON))}
|| {Title, JSON, _, Events} <- Data
] ++
[{Title ++ " (incremental)", ?_assertEqual(Events ++ [end_json], incremental_decode_float_as_string(JSON))}
|| {Title, JSON, _, Events} <- Data
].

parse_test_() ->
Data = test_cases(),
Expand Down
16 changes: 12 additions & 4 deletions src/jsx_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
| {indent, non_neg_integer()}
| {depth, non_neg_integer()}
| {newline, binary()}
| {float_as_string, boolean()}
| legacy_option()
| {legacy_option(), boolean()}.
-type legacy_option() :: strict_comments
Expand Down Expand Up @@ -114,6 +115,8 @@ parse_config([multi_term|Rest], Config) ->
parse_config(Rest, Config#config{multi_term=true});
parse_config([return_tail|Rest], Config) ->
parse_config(Rest, Config#config{return_tail=true});
parse_config([float_as_string|Rest], Config) ->
parse_config(Rest, Config#config{float_as_string=true});
%% retained for backwards compat, now does nothing however
parse_config([repeat_keys|Rest], Config) ->
parse_config(Rest, Config);
Expand Down Expand Up @@ -215,7 +218,8 @@ valid_flags() ->
stream,
uescape,
error_handler,
incomplete_handler
incomplete_handler,
float_as_string
].


Expand Down Expand Up @@ -259,7 +263,8 @@ config_test_() ->
strict_escapes = true,
strict_control_codes = true,
stream = true,
uescape = true
uescape = true,
float_as_string = true
},
parse_config([dirty_strings,
escaped_forward_slashes,
Expand All @@ -270,7 +275,8 @@ config_test_() ->
repeat_keys,
strict,
stream,
uescape
uescape,
float_as_string
])
)
},
Expand Down Expand Up @@ -342,6 +348,7 @@ config_to_list_test_() ->
stream,
uescape,
unescaped_jsonp,
float_as_string,
strict
],
config_to_list(
Expand All @@ -356,7 +363,8 @@ config_to_list_test_() ->
strict_escapes = true,
strict_control_codes = true,
stream = true,
uescape = true
uescape = true,
float_as_string = true
}
)
)},
Expand Down
1 change: 1 addition & 0 deletions src/jsx_config.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
return_tail = false :: boolean(),
uescape = false :: boolean(),
unescaped_jsonp = false :: boolean(),
float_as_string = false :: boolean(),
error_handler = false :: false | jsx_config:handler(),
incomplete_handler = false :: false | jsx_config:handler()
}).
24 changes: 20 additions & 4 deletions src/jsx_decoder.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

%% inline handle_event, format_number and maybe_replace
-compile({inline, [handle_event/3]}).
-compile({inline, [format_number/1]}).
-compile({inline, [format_number/2]}).
-compile({inline, [maybe_replace/2]}).
-compile({inline, [doublequote/5, singlequote/5]}).

Expand Down Expand Up @@ -944,10 +944,14 @@ exp(_, N) -> {finish_float, N}.


finish_number(Rest, Handler, Acc, Stack, Config) ->
maybe_done(Rest, handle_event(format_number(Acc), Handler, Config), Stack, Config).
maybe_done(Rest, handle_event(format_number(Acc, Config), Handler, Config), Stack, Config).

format_number({integer, Acc}) -> {integer, binary_to_integer(Acc)};
format_number({float, Acc}) -> {float, binary_to_float(Acc)}.
format_number({integer, Acc}, _Config) -> {integer, binary_to_integer(Acc)};
format_number({float, Acc}, Config) ->
case Config#config.float_as_string of
false -> {float, binary_to_float(Acc)};
_ -> {string, Acc}
end.

true(<<$r, $u, $e, Rest/binary>>, Handler, Stack, Config) ->
maybe_done(Rest, handle_event({literal, true}, Handler, Config), Stack, Config);
Expand Down Expand Up @@ -1906,4 +1910,16 @@ return_tail_test_() ->
)}
].

decode_float_as_string_test_() ->
[
{"simple_float_as_string", ?_assertEqual(
<<"3.13">>,
jsx:decode(<<"3.13">>, [float_as_string])
)},
{"float_as_string_in_object", ?_assertEqual(
#{<<"a">> => <<"1000000000.5549999">>},
jsx:decode(<<"{\"a\":\"1000000000.5549999\"}">>, [float_as_string])
)}
].

-endif.