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

Compat decimal #634

Merged
merged 3 commits into from Jan 13, 2021
Merged
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
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