Skip to content

Commit

Permalink
Compat decimal (#634)
Browse files Browse the repository at this point in the history
* Add option for compat_bigdecimal
  • Loading branch information
ohler55 committed Jan 13, 2021
1 parent b73e9fe commit 362ce48
Show file tree
Hide file tree
Showing 11 changed files with 80 additions and 27 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,11 @@
# CHANGELOG

## 3.11.0 - 2020-01-12

- Added `:compat_bigdecimal` to support the JSON gem `:decimal_class` undocumented option.

- Reverted the use of `:bigdecimal_load` for `:compat` mode.

## 3.10.18 - 2020-12-25

- Fix modes table by marking compat mode `:bigdecimal_load` instead of `:bigdecimal_as_decimal`.
Expand Down
12 changes: 3 additions & 9 deletions ext/oj/mimic_json.c
Expand Up @@ -364,7 +364,6 @@ mimic_generate_core(int argc, VALUE *argv, Options copts) {
struct _out out;
VALUE rstr;

// TBD
memset(buf, 0, sizeof(buf));

out.buf = buf;
Expand Down Expand Up @@ -510,6 +509,7 @@ mimic_parse_core(int argc, VALUE *argv, VALUE self, bool bang) {
pi.options.create_ok = No;
pi.options.allow_nan = (bang ? Yes : No);
pi.options.nilnil = No;
pi.options.bigdec_load = RubyDec;
pi.options.mode = CompatMode;
pi.max_depth = 100;

Expand Down Expand Up @@ -560,14 +560,7 @@ mimic_parse_core(int argc, VALUE *argv, VALUE self, bool bang) {
}
}
if (Qtrue == rb_funcall(ropts, oj_has_key_id, 1, oj_decimal_class_sym)) {
v = rb_hash_lookup(ropts, oj_decimal_class_sym);
if (rb_cFloat == v) {
pi.options.bigdec_load = FloatDec;
} else if (oj_bigdecimal_class == v) {
pi.options.bigdec_load = BigDec;
} else if (Qnil == v) {
pi.options.bigdec_load = AutoDec;
}
pi.options.compat_bigdec = (oj_bigdecimal_class == rb_hash_lookup(ropts, oj_decimal_class_sym));
}
v = rb_hash_lookup(ropts, oj_max_nesting_sym);
if (Qtrue == v) {
Expand Down Expand Up @@ -693,6 +686,7 @@ static struct _options mimic_object_to_json_options = {
RubyTime, // time_format
No, // bigdec_as_num
RubyDec, // bigdec_load
false, // compat_bigdec
No, // to_hash
No, // to_json
No, // as_json
Expand Down
16 changes: 12 additions & 4 deletions ext/oj/oj.c
Expand Up @@ -107,6 +107,7 @@ static VALUE bigdecimal_load_sym;
static VALUE bigdecimal_sym;
static VALUE circular_sym;
static VALUE class_cache_sym;
static VALUE compat_bigdecimal_sym;
static VALUE compat_sym;
static VALUE create_id_sym;
static VALUE custom_sym;
Expand Down Expand Up @@ -168,6 +169,7 @@ struct _options oj_default_options = {
UnixTime, // time_format
NotSet, // bigdec_as_num
AutoDec, // bigdec_load
false, // compat_bigdec
No, // to_hash
No, // to_json
No, // as_json
Expand Down Expand Up @@ -230,6 +232,7 @@ struct _options oj_default_options = {
* - *:time_format* [_:unix_|_:unix_zone_|_:xmlschema_|_:ruby_] time format when dumping
* - *:bigdecimal_as_decimal* [_Boolean_|_nil_] dump BigDecimal as a decimal number or as a String
* - *:bigdecimal_load* [_:bigdecimal_|_:float_|_:auto_|_:fast_] load decimals as BigDecimal instead of as a Float. :auto pick the most precise for the number of digits. :float should be the same as ruby. :fast may require rounding but is must faster.
* - *:compat_bigdecimal* [_true_|_false_] load decimals as BigDecimal instead of as a Float when in compat or rails mode.
* - *:create_id* [_String_|_nil_] create id for json compatible object encoding, default is 'json_class'
* - *:create_additions* [_Boolean_|_nil_] if true allow creation of instances using create_id on load.
* - *:second_precision* [_Fixnum_|_nil_] number of digits after the decimal when dumping the seconds portion of time
Expand Down Expand Up @@ -334,6 +337,7 @@ get_def_opts(VALUE self) {
case AutoDec:
default: rb_hash_aset(opts, bigdecimal_load_sym, auto_sym); break;
}
rb_hash_aset(opts, compat_bigdecimal_sym, oj_default_options.compat_bigdec ? Qtrue : Qfalse);
rb_hash_aset(opts, create_id_sym, (NULL == oj_default_options.create_id) ? Qnil : rb_str_new2(oj_default_options.create_id));
rb_hash_aset(opts, oj_space_sym, (0 == oj_default_options.dump_opts.after_size) ? Qnil : rb_str_new2(oj_default_options.dump_opts.after_sep));
rb_hash_aset(opts, oj_space_before_sym, (0 == oj_default_options.dump_opts.before_size) ? Qnil : rb_str_new2(oj_default_options.dump_opts.before_sep));
Expand Down Expand Up @@ -379,6 +383,7 @@ get_def_opts(VALUE self) {
* - *:escape* [_:newline_|_:json_|_:xss_safe_|_:ascii_|_unicode_xss_|_nil_] mode encodes all high-bit characters as escaped sequences if :ascii, :json is standand UTF-8 JSON encoding, :newline is the same as :json but newlines are not escaped, :unicode_xss allows unicode but escapes &, <, and >, and any \u20xx characters along with some others, and :xss_safe escapes &, <, and >, and some others.
* - *:bigdecimal_as_decimal* [_Boolean_|_nil_] dump BigDecimal as a decimal number or as a String.
* - *:bigdecimal_load* [_:bigdecimal_|_:float_|_:auto_|_nil_] load decimals as BigDecimal instead of as a Float. :auto pick the most precise for the number of digits.
* - *:compat_bigdecimal* [_true_|_false_] load decimals as BigDecimal instead of as a Float in compat mode.
* - *:mode* [_:object_|_:strict_|_:compat_|_:null_|_:custom_|_:rails_|_:wab_] load and dump mode to use for JSON :strict raises an exception when a non-supported Object is encountered. :compat attempts to extract variable values from an Object using to_json() or to_hash() then it walks the Object's variables if neither is found. The :object mode ignores to_hash() and to_json() methods and encodes variables using code internal to the Oj gem. The :null mode ignores non-supported Objects and replaces them with a null. The :custom mode honors all dump options. The :rails more mimics rails and Active behavior.
* - *:time_format* [_:unix_|_:xmlschema_|_:ruby_] time format when dumping in :compat mode :unix decimal number denoting the number of seconds since 1/1/1970, :unix_zone decimal number denoting the number of seconds since 1/1/1970 plus the utc_offset in the exponent, :xmlschema date-time format taken from XML Schema as a String, :ruby Time.to_s formatted String.
* - *:create_id* [_String_|_nil_] create id for json compatible object encoding
Expand Down Expand Up @@ -582,19 +587,21 @@ oj_parse_options(VALUE ropts, Options copts) {
rb_raise(rb_eArgError, ":bigdecimal_load must be :bigdecimal, :float, or :auto.");
}
}
if (Qnil != (v = rb_hash_lookup(ropts, compat_bigdecimal_sym))) {
copts->compat_bigdec = (Qtrue == v);
}
if (Qtrue == rb_funcall(ropts, oj_has_key_id, 1, oj_decimal_class_sym)) {
v = rb_hash_lookup(ropts, oj_decimal_class_sym);
if (rb_cFloat == v) {
copts->bigdec_load = FloatDec;
copts->compat_bigdec = FloatDec;
} else if (oj_bigdecimal_class == v) {
copts->bigdec_load = BigDec;
copts->compat_bigdec = BigDec;
} else if (Qnil == v) {
copts->bigdec_load = AutoDec;
copts->compat_bigdec = AutoDec;
} else {
rb_raise(rb_eArgError, ":decimal_class must be BigDecimal, Float, or nil.");
}
}

if (Qtrue == rb_funcall(ropts, oj_has_key_id, 1, create_id_sym)) {
v = rb_hash_lookup(ropts, create_id_sym);
if (Qnil == v) {
Expand Down Expand Up @@ -1663,6 +1670,7 @@ Init_oj() {
bigdecimal_sym = ID2SYM(rb_intern("bigdecimal")); rb_gc_register_address(&bigdecimal_sym);
circular_sym = ID2SYM(rb_intern("circular")); rb_gc_register_address(&circular_sym);
class_cache_sym = ID2SYM(rb_intern("class_cache")); rb_gc_register_address(&class_cache_sym);
compat_bigdecimal_sym = ID2SYM(rb_intern("compat_bigdecimal"));rb_gc_register_address(&compat_bigdecimal_sym);
compat_sym = ID2SYM(rb_intern("compat")); rb_gc_register_address(&compat_sym);
create_id_sym = ID2SYM(rb_intern("create_id")); rb_gc_register_address(&create_id_sym);
custom_sym = ID2SYM(rb_intern("custom")); rb_gc_register_address(&custom_sym);
Expand Down
1 change: 1 addition & 0 deletions ext/oj/oj.h
Expand Up @@ -135,6 +135,7 @@ typedef struct _options {
char time_format; // TimeFormat
char bigdec_as_num; // YesNo
char bigdec_load; // BigLoad
char compat_bigdec; // boolean (0 or 1)
char to_hash; // YesNo
char to_json; // YesNo
char as_json; // YesNo
Expand Down
15 changes: 12 additions & 3 deletions ext/oj/parse.c
Expand Up @@ -385,8 +385,13 @@ read_num(ParseInfo pi) {
ni.nan = 0;
ni.neg = 0;
ni.has_exp = 0;
ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
if (CompatMode == pi->options.mode) {
ni.no_big = !pi->options.compat_bigdec;
ni.bigdec_load = pi->options.compat_bigdec;
} else {
ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
}

if ('-' == *pi->cur) {
pi->cur++;
Expand Down Expand Up @@ -511,7 +516,11 @@ read_num(ParseInfo pi) {
ni.nan = 1;
}
}
if (BigDec == pi->options.bigdec_load) {
if (CompatMode == pi->options.mode) {
if (pi->options.compat_bigdec) {
ni.big = 1;
}
} else if (BigDec == pi->options.bigdec_load) {
ni.big = 1;
}
if (0 == parent) {
Expand Down
41 changes: 33 additions & 8 deletions ext/oj/sparse.c
Expand Up @@ -400,8 +400,13 @@ read_num(ParseInfo pi) {
ni.nan = 0;
ni.neg = 0;
ni.has_exp = 0;
ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
if (CompatMode == pi->options.mode) {
ni.no_big = !pi->options.compat_bigdec;
ni.bigdec_load = pi->options.compat_bigdec;
} else {
ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
}

c = reader_get(&pi->rd);
if ('-' == c) {
Expand Down Expand Up @@ -518,7 +523,11 @@ read_num(ParseInfo pi) {
ni.nan = 1;
}
}
if (BigDec == pi->options.bigdec_load) {
if (CompatMode == pi->options.mode) {
if (pi->options.compat_bigdec) {
ni.big = 1;
}
} else if (BigDec == pi->options.bigdec_load) {
ni.big = 1;
}
add_num_value(pi, &ni);
Expand All @@ -541,15 +550,24 @@ read_nan(ParseInfo pi) {
ni.infinity = 0;
ni.nan = 1;
ni.neg = 0;
ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
if (CompatMode == pi->options.mode) {
ni.no_big = !pi->options.compat_bigdec;
ni.bigdec_load = pi->options.compat_bigdec;
} else {
ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
}

if ('a' != reader_get(&pi->rd) ||
('N' != (c = reader_get(&pi->rd)) && 'n' != c)) {
oj_set_error_at(pi, oj_parse_error_class, __FILE__, __LINE__, "not a number or other value");
return;
}
if (BigDec == pi->options.bigdec_load) {
if (CompatMode == pi->options.mode) {
if (pi->options.compat_bigdec) {
ni.big = 1;
}
} else if (BigDec == pi->options.bigdec_load) {
ni.big = 1;
}
add_num_value(pi, &ni);
Expand Down Expand Up @@ -739,8 +757,15 @@ oj_sparse2(ParseInfo pi) {
ni.infinity = 0;
ni.nan = 1;
ni.neg = 0;
ni.no_big = (FloatDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
if (CompatMode == pi->options.mode) {
ni.no_big = !pi->options.compat_bigdec;
ni.bigdec_load = pi->options.compat_bigdec;
} else {
ni.no_big = (FloatDec == pi->options.bigdec_load ||
FastDec == pi->options.bigdec_load ||
RubyDec == pi->options.bigdec_load);
ni.bigdec_load = pi->options.bigdec_load;
}
add_num_value(pi, &ni);
} else {
oj_set_error_at(pi, oj_parse_error_class, __FILE__, __LINE__, "invalid token");
Expand Down
2 changes: 1 addition & 1 deletion lib/oj/version.rb
@@ -1,5 +1,5 @@

module Oj
# Current version of the module.
VERSION = '3.10.18'
VERSION = '3.11.0'
end
3 changes: 2 additions & 1 deletion pages/Modes.md
Expand Up @@ -95,7 +95,8 @@ information.
| :ascii_only | Boolean | x | x | 2 | 2 | x | x | |
| :auto_define | Boolean | | | | | x | x | |
| :bigdecimal_as_decimal | Boolean | | | | 3 | x | x | |
| :bigdecimal_load | Boolean | | | x | | | x | |
| :bigdecimal_load | Boolean | | | | | | x | |
| :compat_bigdecimal | Boolean | | | x | | | x | |
| :circular | Boolean | x | x | x | x | x | x | |
| :class_cache | Boolean | | | | | x | x | |
| :create_additions | Boolean | | | x | x | | x | |
Expand Down
8 changes: 8 additions & 0 deletions pages/Options.md
Expand Up @@ -70,6 +70,14 @@ This can also be set with `:decimal_class` when used as a load or
parse option to match the JSON gem. In that case either `Float`,
`BigDecimal`, or `nil` can be provided.

### :compat_bigdecimal [Boolean]

Determines how to load decimals when in `:compat` mode.

- `true` convert all decimal numbers to BigDecimal.

- `false` convert all decimal numbers to Float.

### :circular [Boolean]

Detect circular references while dumping. In :compat mode raise a
Expand Down
2 changes: 1 addition & 1 deletion test/test_compat.rb
Expand Up @@ -283,7 +283,7 @@ def test_bigdecimal
assert_equal('"0.314159265358979323846e1"', json.downcase)
end

def test_bigdecimal_load
def test_decimal_class
big = BigDecimal('3.14159265358979323846')
# :decimal_class is the undocumented feature.
json = Oj.load('3.14159265358979323846', mode: :compat, decimal_class: BigDecimal)
Expand Down
1 change: 1 addition & 0 deletions test/test_various.rb
Expand Up @@ -120,6 +120,7 @@ def test_set_options
escape_mode: :ascii,
time_format: :unix_zone,
bigdecimal_load: :float,
compat_bigdecimal: true,
create_id: 'classy',
create_additions: true,
space: 'z',
Expand Down

0 comments on commit 362ce48

Please sign in to comment.