Skip to content

Commit

Permalink
fixed unix time bug that failed to handle times before 1970
Browse files Browse the repository at this point in the history
  • Loading branch information
ohler55 committed Jan 15, 2013
1 parent 816f316 commit 45baf12
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 11 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ A fast JSON parser and Object marshaller as a Ruby gem.

## <a name="release">Release Notes</a>

### Release 2.0.0
### Release 2.0.1

- Thanks to yuki24 Floats are now output with a decimal even if they are an integer value.
- BigDecimals now dump to a string in compat mode thanks to cgriego.

- <b>The Simple API for JSON (SAJ) API has been added. Read more about it on the [Oj::Saj page](http://www.ohler.com/oj/Oj/Saj.html).</b>
- High precision time (nano time) can be turned off for better compatibility with other JSON parsers.

## <a name="description">Description</a>

Expand Down
54 changes: 46 additions & 8 deletions ext/oj/dump.c
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,7 @@ dump_time(VALUE obj, Out out) {
char *b = buf + sizeof(buf) - 1;
long size;
char *dot = b - 10;
int neg = 0;
#if HAS_RB_TIME_TIMESPEC
struct timespec ts = rb_time_timespec(obj);
time_t sec = ts.tv_sec;
Expand All @@ -941,13 +942,38 @@ dump_time(VALUE obj, Out out) {
#endif
#endif

if (0 > sec) {
neg = 1;
sec = -sec;
if (0 < nsec) {
nsec = 1000000000 - nsec;
sec--;
}
}
*b-- = '\0';
for (; dot < b; b--, nsec /= 10) {
*b = '0' + (nsec % 10);
if (0 < out->opts->sec_prec) {
if (9 > out->opts->sec_prec) {
int i;

for (i = 9 - out->opts->sec_prec; 0 < i; i--) {
dot++;
nsec = (nsec + 5) / 10;
}
}
for (; dot < b; b--, nsec /= 10) {
*b = '0' + (nsec % 10);
}
*b-- = '.';
}
*b-- = '.';
for (; 0 < sec; b--, sec /= 10) {
*b = '0' + (sec % 10);
if (0 == sec) {
*b-- = '0';
} else {
for (; 0 < sec; b--, sec /= 10) {
*b = '0' + (sec % 10);
}
}
if (neg) {
*b-- = '-';
}
b++;
size = sizeof(buf) - (b - buf) - 1;
Expand Down Expand Up @@ -1012,7 +1038,7 @@ dump_xml_time(VALUE obj, Out out) {
tzmin = (int)(tm->tm_gmtoff / 60) - (tzhour * 60);
}
#endif
if (0 == nsec) {
if (0 == nsec || 0 == out->opts->sec_prec) {
if (0 == tzhour && 0 == tzmin) {
sprintf(buf, "%04d-%02d-%02dT%02d:%02d:%02dZ",
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
Expand All @@ -1026,11 +1052,23 @@ dump_xml_time(VALUE obj, Out out) {
dump_cstr(buf, 25, 0, 0, out);
}
} else {
sprintf(buf, "%04d-%02d-%02dT%02d:%02d:%02d.%09ld%c%02d:%02d",
char format[64] = "%04d-%02d-%02dT%02d:%02d:%02d.%09ld%c%02d:%02d";
int len = 35;

if (9 > out->opts->sec_prec) {
int i;

format[32] = '0' + out->opts->sec_prec;
for (i = 9 - out->opts->sec_prec; 0 < i; i--) {
nsec = (nsec + 5) / 10;
len--;
}
}
sprintf(buf, format,
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
tm->tm_hour, tm->tm_min, tm->tm_sec, nsec,
tzsign, tzhour, tzmin);
dump_cstr(buf, 35, 0, 0, out);
dump_cstr(buf, len, 0, 0, out);
}
}

Expand Down
12 changes: 12 additions & 0 deletions ext/oj/load.c
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,12 @@ static VALUE
read_time(ParseInfo pi) {
time_t v = 0;
long v2 = 0;
int neg = 0;

if ('-' == *pi->s) {
pi->s++;
neg = 1;
}
for (; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
v = v * 10 + (*pi->s - '0');
}
Expand All @@ -840,6 +845,13 @@ read_time(ParseInfo pi) {
v2 *= 10;
}
}
if (neg) {
v = -v;
if (0 < v2) {
v--;
v2 = 1000000000 - v2;
}
}
#if HAS_NANO_TIME
return rb_time_nano_new(v, v2);
#else
Expand Down
33 changes: 33 additions & 0 deletions ext/oj/oj.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ static VALUE mode_sym;
static VALUE null_sym;
static VALUE object_sym;
static VALUE ruby_sym;
static VALUE sec_prec_sym;
static VALUE strict_sym;
static VALUE symbol_keys_sym;
static VALUE time_format_sym;
Expand Down Expand Up @@ -128,6 +129,7 @@ struct _Options oj_default_options = {
UnixTime, // time_format
json_class, // create_id
65536, // max_stack
9, // sec_prec
0, // dump_opts
};

Expand Down Expand Up @@ -157,13 +159,15 @@ oj_get_odd(VALUE clas) {
* - time_format: [:unix|:xmlschema|:ruby] time format when dumping in :compat mode
* - create_id: [String|nil] create id for json compatible object encoding, default is 'json_create'
* - max_stack: [Fixnum|nil] maximum json size to allocate on the stack, default is 65536
* - second_precision: [Fixnum|nil] number of digits after the decimal when dumping the seconds portion of time
* @return [Hash] all current option settings.
*/
static VALUE
get_def_opts(VALUE self) {
VALUE opts = rb_hash_new();

rb_hash_aset(opts, indent_sym, INT2FIX(oj_default_options.indent));
rb_hash_aset(opts, sec_prec_sym, INT2FIX(oj_default_options.sec_prec));
rb_hash_aset(opts, max_stack_sym, INT2FIX(oj_default_options.max_stack));
rb_hash_aset(opts, circular_sym, (Yes == oj_default_options.circular) ? Qtrue : ((No == oj_default_options.circular) ? Qfalse : Qnil));
rb_hash_aset(opts, auto_define_sym, (Yes == oj_default_options.auto_define) ? Qtrue : ((No == oj_default_options.auto_define) ? Qfalse : Qnil));
Expand Down Expand Up @@ -210,6 +214,7 @@ get_def_opts(VALUE self) {
* :ruby Time.to_s formatted String
* @param [String|nil] :create_id create id for json compatible object encoding
* @param [Fixnum|nil] :max_stack maximum size to allocate on the stack for a JSON String
* @param [Fixnum|nil] :second_precision number of digits after the decimal when dumping the seconds portion of time
* @return [nil]
*/
static VALUE
Expand All @@ -230,6 +235,19 @@ set_def_opts(VALUE self, VALUE opts) {
Check_Type(v, T_FIXNUM);
oj_default_options.indent = FIX2INT(v);
}
v = rb_hash_aref(opts, sec_prec_sym);
if (Qnil != v) {
int n;

Check_Type(v, T_FIXNUM);
n = FIX2INT(v);
if (0 > n) {
n = 0;
} else if (9 < n) {
n = 9;
}
oj_default_options.sec_prec = n;
}
v = rb_hash_aref(opts, max_stack_sym);
if (Qnil != v) {
int i;
Expand Down Expand Up @@ -323,6 +341,20 @@ parse_options(VALUE ropts, Options copts) {
}
copts->indent = NUM2INT(v);
}
if (Qnil != (v = rb_hash_lookup(ropts, sec_prec_sym))) {
int n;

if (rb_cFixnum != rb_obj_class(v)) {
rb_raise(rb_eArgError, ":second_precision must be a Fixnum.");
}
n = NUM2INT(v);
if (0 > n) {
n = 0;
} else if (9 < n) {
n = 9;
}
copts->sec_prec = n;
}
if (Qnil != (v = rb_hash_lookup(ropts, mode_sym))) {
if (object_sym == v) {
copts->mode = ObjectMode;
Expand Down Expand Up @@ -1009,6 +1041,7 @@ void Init_oj() {
null_sym = ID2SYM(rb_intern("null")); rb_gc_register_address(&null_sym);
object_sym = ID2SYM(rb_intern("object")); rb_gc_register_address(&object_sym);
ruby_sym = ID2SYM(rb_intern("ruby")); rb_gc_register_address(&ruby_sym);
sec_prec_sym = ID2SYM(rb_intern("second_precision"));rb_gc_register_address(&sec_prec_sym);
strict_sym = ID2SYM(rb_intern("strict")); rb_gc_register_address(&strict_sym);
symbol_keys_sym = ID2SYM(rb_intern("symbol_keys")); rb_gc_register_address(&symbol_keys_sym);
time_format_sym = ID2SYM(rb_intern("time_format")); rb_gc_register_address(&time_format_sym);
Expand Down
1 change: 1 addition & 0 deletions ext/oj/oj.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ typedef struct _Options {
char time_format; // TimeFormat
const char *create_id; // 0 or string
size_t max_stack; // max size to allocate on the stack
int sec_prec; // second precision when dumping time
DumpOpts dump_opts;
} *Options;

Expand Down
47 changes: 47 additions & 0 deletions test/tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class Juice < ::Test::Unit::TestCase
def test0_get_options
opts = Oj.default_options()
assert_equal({ :indent=>0,
:second_precision=>9,
:circular=>false,
:auto_define=>true,
:symbol_keys=>false,
Expand All @@ -123,6 +124,7 @@ def test0_get_options
def test0_set_options
orig = {
:indent=>0,
:second_precision=>9,
:circular=>false,
:auto_define=>true,
:symbol_keys=>false,
Expand All @@ -133,6 +135,7 @@ def test0_set_options
:create_id=>'json_class'}
o2 = {
:indent=>4,
:second_precision=>7,
:circular=>true,
:auto_define=>false,
:symbol_keys=>true,
Expand Down Expand Up @@ -265,6 +268,22 @@ def test_unix_time_compat
json = Oj.dump(t, :mode => :compat)
assert_equal(%{1325775487.123456000}, json)
end
def test_unix_time_compat_precision
t = Time.xmlschema("2012-01-05T23:58:07.123456789+09:00")
#t = Time.local(2012, 1, 5, 23, 58, 7, 123456)
json = Oj.dump(t, :mode => :compat, :second_precision => 5)
assert_equal(%{1325775487.12346}, json)
end
def test_unix_time_compat_early
t = Time.xmlschema("1954-01-05T00:00:00.123456789+00:00")
json = Oj.dump(t, :mode => :compat, :second_precision => 5)
assert_equal(%{-504575999.87654}, json)
end
def test_unix_time_compat_1970
t = Time.xmlschema("1970-01-01T00:00:00.123456789+00:00")
json = Oj.dump(t, :mode => :compat, :second_precision => 5)
assert_equal(%{0.12346}, json)
end
def test_ruby_time_compat
t = Time.xmlschema("2012-01-05T23:58:07.123456000+09:00")
json = Oj.dump(t, :mode => :compat, :time_format => :ruby)
Expand Down Expand Up @@ -309,6 +328,25 @@ def test_xml_time_compat_no_secs
assert_equal(%{"2012-01-05T23:58:07%s%02d:%02d"} % [sign, tz / 3600, tz / 60 % 60], json)
end
end
def test_xml_time_compat_precision
begin
t = Time.new(2012, 1, 5, 23, 58, 7.123456789, 32400)
json = Oj.dump(t, :mode => :compat, :time_format => :xmlschema, :second_precision => 5)
assert_equal(%{"2012-01-05T23:58:07.12346+09:00"}, json)
rescue Exception
# some Rubies (1.8.7) do not allow the timezome to be set
t = Time.local(2012, 1, 5, 23, 58, 7.123456789, 0)
json = Oj.dump(t, :mode => :compat, :time_format => :xmlschema, :second_precision => 5)
tz = t.utc_offset
# Ruby does not handle a %+02d properly so...
sign = '+'
if 0 > tz
sign = '-'
tz = -tz
end
assert_equal(%{"2012-01-05T23:58:07.12346%s%02d:%02d"} % [sign, tz / 3600, tz / 60 % 60], json)
end
end
def test_xml_time_compat_zulu
begin
t = Time.new(2012, 1, 5, 23, 58, 7.0, 0)
Expand All @@ -327,6 +365,11 @@ def test_time_object
Oj.default_options = { :mode => :object }
dump_and_load(t, false)
end
def test_time_object_early
t = Time.xmlschema("1954-01-05T00:00:00.123456789+00:00")
Oj.default_options = { :mode => :object }
dump_and_load(t, false)
end

# Class
def test_class_strict
Expand Down Expand Up @@ -670,6 +713,10 @@ def test_bigdecimal_compat
json = Oj.dump(orig, :mode => :compat)
bg = Oj.load(json, :mode => :compat)
assert_equal(orig.to_s, bg)
orig = BigDecimal.new('3.14159265358979323846')
json = Oj.dump(orig, :mode => :compat)
bg = Oj.load(json, :mode => :compat)
assert_equal(orig.to_s, bg)
end
def test_bigdecimal_object
mode = Oj.default_options[:mode]
Expand Down

0 comments on commit 45baf12

Please sign in to comment.