From 231e7c032666108b2d5f1f9b4cb012fb580fdffe Mon Sep 17 00:00:00 2001 From: Yourkov Mikhail Date: Fri, 13 Dec 2013 23:49:25 +0400 Subject: [PATCH 1/2] Option to decode numerals to decimal format JSON non-integer numerals as decoded to {Sign, Value, Scale} tuple instead of floating-point value, if {number_format, decimal} option is given to jsonx:decode/2 --- c_src/decoder.c | 78 +++++++++++++++++++++++++++++++++++++++++++--- c_src/jsonx.c | 6 ++-- c_src/jsonx.h | 3 ++ src/jsonx.erl | 41 +++++++++++------------- test/num_tests.erl | 16 ++++++++++ 5 files changed, 116 insertions(+), 28 deletions(-) diff --git a/c_src/decoder.c b/c_src/decoder.c index f3b9f41..4d09a84 100644 --- a/c_src/decoder.c +++ b/c_src/decoder.c @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/c_src/jsonx.c b/c_src/jsonx.c index a4e1809..4b214c9 100644 --- a/c_src/jsonx.c +++ b/c_src/jsonx.c @@ -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; @@ -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} }; diff --git a/c_src/jsonx.h b/c_src/jsonx.h index 5e14ff7..539325c 100644 --- a/c_src/jsonx.h +++ b/c_src/jsonx.h @@ -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; diff --git a/src/jsonx.erl b/src/jsonx.erl index 9b8e2d0..684ded8 100644 --- a/src/jsonx.erl +++ b/src/jsonx.erl @@ -74,7 +74,7 @@ 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 @@ -82,10 +82,8 @@ decode(JSON) -> OPTIONS :: [{format, struct|eep18|proplist}], 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 @@ -94,7 +92,7 @@ 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 @@ -105,10 +103,8 @@ 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 @@ -120,10 +116,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) -> @@ -136,16 +132,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 diff --git a/test/num_tests.erl b/test/num_tests.erl index b24e4a3..bb3748c 100644 --- a/test/num_tests.erl +++ b/test/num_tests.erl @@ -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}]). From 54f566b61c4ba584f8f858e4d3e110fa2537eb50 Mon Sep 17 00:00:00 2001 From: Yourkov Mikhail Date: Sat, 14 Dec 2013 00:39:33 +0400 Subject: [PATCH 2/2] Mention decimal in documentation --- README.md | 2 ++ src/jsonx.erl | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 133a18b..63fee43 100644 --- a/README.md +++ b/README.md @@ -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) ----------------------- diff --git a/src/jsonx.erl b/src/jsonx.erl index 684ded8..f00c7c7 100644 --- a/src/jsonx.erl +++ b/src/jsonx.erl @@ -10,6 +10,7 @@ %%
  • false -> atom false
  • %%
  • string -> binary
  • %%
  • number -> number
  • +%%
  • number -> {Sign, Value, Scale}, if non-integer and number_format is decimal (Sign = 0 for positive, 1 for negative)
  • %%
  • array -> list
  • %%
  • object -> {PropList}, optional struct or proplist.
  • %%
  • object -> #record{...} - decoder must be predefined
  • @@ -79,7 +80,7 @@ decode(JSON) -> %%@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) -> {Object, Float} = parse_format(Options), @@ -97,7 +98,7 @@ decoder(Records_desc) -> %%@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),