Skip to content

Option to decode fractional numbers to decimal #15

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

Open
wants to merge 2 commits into
base: master
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ Mapping (JSON -> Erlang)
{"this": "json"} :-> [{<<"this">>: <<"json">>}] %% optional proplist
{"this": "json"} :-> {struct, [{<<"this">>: <<"json">>}]} %% optional struct
JSONObject :-> #rec{...} %% decoder must be predefined
-1.123e12 :-> {1, 1123, 9} %% optional {number_format, decimal}
1.123e-6 :-> {0, 1123, -9} %% optional {number_format, decimal}

Mapping (Erlang -> JSON)
-----------------------
Expand Down
78 changes: 74 additions & 4 deletions c_src/decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ typedef struct{
size_t offset;
ERL_NIF_TERM input;
ERL_NIF_TERM format; //struct, eep18, proplist
ERL_NIF_TERM number_format; //float, decimal
ERL_NIF_TERM error;
ERL_NIF_TERM *stack_top;
ERL_NIF_TERM *stack_down;
Expand Down Expand Up @@ -314,6 +315,71 @@ parse_string_as_existing_atom(State* st){
return (ERL_NIF_TERM)0;
}

static inline ERL_NIF_TERM
parse_decimal(State *st){
long long value;
long long scale;
long long sign;
char *endptr;

char *sub_start;
long long sub;
long long i;

value = strtoll((char *)st->cur, &endptr, 10);
if (value < 0) {
sign = 1;
value = -value;
} else
sign = 0;

if (*endptr == '.') {
sub_start = endptr + 1;
if(*sub_start == '+' || *sub_start == '-')
return (ERL_NIF_TERM)0;

sub = strtoll(sub_start, &endptr, 10);
if(sub_start == endptr){
return (ERL_NIF_TERM)0;
}else if(errno == ERANGE){
st->error = st->priv->am_erange;
return (ERL_NIF_TERM)0;
}

scale = sub_start - endptr;
for (i = scale; i < 0; i++)
value *= 10;
value += sub;

if(value < 0) {
st->error = st->priv->am_erange;
return (ERL_NIF_TERM)0;
}
} else {
scale = 0;
}

if ((*endptr | 0x20) == 'e') {
sub_start = endptr + 1;
sub = strtoll(sub_start, &endptr, 10);
if(sub_start == endptr){
return (ERL_NIF_TERM)0;
}else if(errno == ERANGE){
st->error = st->priv->am_erange;
return (ERL_NIF_TERM)0;
}
scale += sub;
}

st->cur = (unsigned char*)endptr;
return enif_make_tuple3(
st->env,
enif_make_int64(st->env, sign),
enif_make_int64(st->env, value),
enif_make_int64(st->env, scale)
);
}

static inline ERL_NIF_TERM
parse_number(State *st){
long long int_num;
Expand All @@ -330,6 +396,9 @@ parse_number(State *st){
}

if(*endptr == '.' || *endptr == 'e' || *endptr == 'E'){
if(st->number_format == st->priv->am_decimal){
return parse_decimal(st);
}
float_num = strtod((char *)st->cur, &endptr);
if(errno != ERANGE){
st->cur = (unsigned char*)endptr;
Expand Down Expand Up @@ -403,15 +472,16 @@ decode_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]){
if(!enif_inspect_binary(env, argv[0], &input)){
return enif_make_badarg(env);
}
assert(argc == 2 || argc == 4 );
assert(argc == 3 || argc == 5 );
State st;
st.priv = (PrivData*)enif_priv_data(env);
st.resource = NULL;
st.input = argv[0];
st.format = argv[1];
if (argc == 4){ // whith resource
assert(enif_get_resource(env, argv[2], st.priv->decoder_RSTYPE, (void**)&st.resource));
st.strict_flag = enif_is_identical(st.priv->am_true, argv[3]) ? 1 : 0;
st.number_format = argv[2];
if (argc == 5){ // whith resource
assert(enif_get_resource(env, argv[3], st.priv->decoder_RSTYPE, (void**)&st.resource));
st.strict_flag = enif_is_identical(st.priv->am_true, argv[4]) ? 1 : 0;
}
st.offset = JS_OFFSET;
st.buf_size = st.offset + input.size + 4;
Expand Down
6 changes: 4 additions & 2 deletions c_src/jsonx.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info){
if(!enif_make_existing_atom(env, "eep18", &(pdata->am_eep18), ERL_NIF_LATIN1)) return 1;
if(!enif_make_existing_atom(env, "no_match", &(pdata->am_no_match), ERL_NIF_LATIN1)) return 1;

if(!enif_make_existing_atom(env, "decimal", &(pdata->am_decimal), ERL_NIF_LATIN1)) return 1;
if(!enif_make_existing_atom(env, "float", &(pdata->am_float), ERL_NIF_LATIN1)) return 1;
*priv_data = (void*)pdata;

return 0;
Expand Down Expand Up @@ -223,8 +225,8 @@ static ErlNifFunc
nif_funcs[] = {
{"encode1", 1, encode_nif},
{"encode_res", 2, encode_nif}, // with resource
{"decode_opt", 2, decode_nif}, // with options
{"decode_res", 4, decode_nif}, // with options, resource and strict flag
{"decode_opt", 3, decode_nif}, // with options
{"decode_res", 5, decode_nif}, // with options, resource and strict flag
{"make_encoder_resource", 7, make_encoder_resource_nif},
{"make_decoder_resource", 6, make_decoder_resource_nif}
};
Expand Down
3 changes: 3 additions & 0 deletions c_src/jsonx.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ typedef struct{
ERL_NIF_TERM am_eep18;
ERL_NIF_TERM am_no_match;

ERL_NIF_TERM am_decimal;
ERL_NIF_TERM am_float;

ErlNifResourceType* encoder_RSTYPE;
ErlNifResourceType* decoder_RSTYPE;
}PrivData;
Expand Down
46 changes: 22 additions & 24 deletions src/jsonx.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
%% <li>false -> atom false</li>
%% <li>string -> binary</li>
%% <li>number -> number</li>
%% <li>number -> {Sign, Value, Scale}, if non-integer and number_format is decimal (Sign = 0 for positive, 1 for negative)</li>
%% <li>array -> list</li>
%% <li>object -> {PropList}, optional struct or proplist.</li>
%% <li>object -> #record{...} - decoder must be predefined</li>
Expand Down Expand Up @@ -74,18 +75,16 @@ encoder(Records_desc, Options) ->
JSON :: binary(),
JSON_TERM :: any().
decode(JSON) ->
decode_opt(JSON, eep18).
decode_opt(JSON, eep18, float).

%%@doc Decode JSON to Erlang term with options.
-spec decode(JSON, OPTIONS) -> JSON_TERM when
JSON :: binary(),
OPTIONS :: [{format, struct|eep18|proplist}],
OPTIONS :: [{format, struct|eep18|proplist} | {number_format, float|decimal}],
JSON_TERM :: any().
decode(JSON, Options) ->
case parse_format(Options) of
undefined -> decode_opt(JSON, eep18);
F -> decode_opt(JSON, F)
end.
{Object, Float} = parse_format(Options),
decode_opt(JSON, Object, Float).

%%@doc Build a JSON decoder.
-spec decoder(RECORDS_DESC) -> DECODER when
Expand All @@ -94,21 +93,19 @@ decode(JSON, Options) ->
decoder(Records_desc) ->
{RecCnt, UKeyCnt, KeyCnt, UKeys, Keys, Records3} = prepare_for_dec(Records_desc),
Resource = make_decoder_resource(RecCnt, UKeyCnt, KeyCnt, UKeys, Keys, Records3),
fun(JSON_TERM) -> decode_res(JSON_TERM, eep18, Resource, true) end.
fun(JSON_TERM) -> decode_res(JSON_TERM, eep18, decimal, Resource, true) end.

%%@doc Build a JSON decoder with output undefined objects.
-spec decoder(RECORDS_DESC, OPTIONS) -> DECODER when
RECORDS_DESC :: [{tag, [names]}],
OPTIONS :: [{format, struct|eep18|proplist}],
OPTIONS :: [{format, struct|eep18|proplist} | {number_format, float|decimal}],
DECODER :: function().
decoder(Records_desc, Options) ->
{RecCnt, UKeyCnt, KeyCnt, UKeys, Keys, Records3} = prepare_for_dec(Records_desc),
Resource = make_decoder_resource(RecCnt, UKeyCnt, KeyCnt, UKeys, Keys, Records3),
%%Format = parse_format(Options),
case parse_format(Options) of
undefined -> fun(JSON_TERM) -> decode_res(JSON_TERM, eep18, Resource, false) end;
Format -> fun(JSON_TERM) -> decode_res(JSON_TERM, Format, Resource, false) end
end.
{Object, Float} = parse_format(Options),
fun(JSON_TERM) -> decode_res(JSON_TERM, Object, Float, Resource, false) end.

%% ==========
%% Call NIFs
Expand All @@ -120,10 +117,10 @@ encode1(_JSON_TERM) ->
encode_res(_JSON_TERM, _RESOURCE) ->
not_loaded(?LINE).

decode_opt(_JSON, _FORMAT) ->
decode_opt(_JSON, _FORMAT, _NUMBER_FORMAT) ->
not_loaded(?LINE).

decode_res(_JSON_TERM, _FORMAT, _RESOURCE, _STRICT_FLAG) ->
decode_res(_JSON_TERM, _FORMAT, _NUMBER_FORMAT, _RESOURCE, _STRICT_FLAG) ->
not_loaded(?LINE).

make_encoder_resource(_Rcnt, _Fcnt, _Records, _Fields, _Binsz, _Bin, _Ignored) ->
Expand All @@ -136,16 +133,17 @@ make_decoder_resource(_RecCnt, _UKeyCnt, _KeyCnt, _UKeys, _Keys, _Records3) ->
%% Private functions
%% =================

parse_format([]) ->
undefined;
parse_format([{format, struct} | _]) ->
struct;
parse_format([{format, proplist} | _]) ->
proplist;
parse_format([{format, eep18} | _]) ->
eep18;
parse_format([_H | T]) ->
parse_format(T).
parse_format(X) ->
% options list reversed to satisfy obj_tests:dec2obj4_test
parse_format(lists:reverse(X), eep18, float).
parse_format([], Object, Float) ->
{Object, Float};
parse_format([{format, Format} | T], _, Float) when Format == struct orelse Format == proplist orelse Format == eep18 ->
parse_format(T, Format, Float);
parse_format([{number_format, Format} | T], Object, _) when Format == float orelse Format == decimal ->
parse_format(T, Object, Format);
parse_format([_ | T], Object, Float) ->
parse_format(T, Object, Float).

%%%% Internal for decoder

Expand Down
16 changes: 16 additions & 0 deletions test/num_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,19 @@ decnum00_test() ->
0.0 = jsonx:decode(<<"0.0">>).
decnum1_test() ->
-0.0012 = jsonx:decode(<<"-1.2e-3">>).

%% Test decode numerics to decimal
decnum_decimal_integer_test() ->
123 = jsonx:decode(<<"123">>, [{number_format, decimal}]).
decnum_decimal_zero_test() ->
{0, 0, -1} = jsonx:decode(<<"0.0">>, [{number_format, decimal}]).
decnum_decimal_frac_exp_test() ->
{1,12,5} = jsonx:decode(<<"-1.2e6">>, [{number_format, decimal}]).
decnum_decimal_frac_neg_exp_test() ->
{1,12,-4} = jsonx:decode(<<"-1.2e-3">>, [{number_format, decimal}]).
decnum_decimal_frac_neg_exp_case_test() ->
{1,12,-4} = jsonx:decode(<<"-1.2E-3">>, [{number_format, decimal}]).
decnum_decimal_frac_test() ->
{0,12,-1} = jsonx:decode(<<"1.2">>, [{number_format, decimal}]).
decnum_decimal_exp_test() ->
{0,1,2} = jsonx:decode(<<"1e2">>, [{number_format, decimal}]).