diff options
-rw-r--r-- | benchmark/time_xmlschema.yml | 23 | ||||
-rw-r--r-- | spec/ruby/core/time/iso8601_spec.rb | 6 | ||||
-rw-r--r-- | spec/ruby/core/time/shared/xmlschema.rb | 31 | ||||
-rw-r--r-- | spec/ruby/core/time/xmlschema_spec.rb | 6 | ||||
-rw-r--r-- | spec/ruby/library/time/iso8601_spec.rb | 2 | ||||
-rw-r--r-- | spec/ruby/library/time/shared/xmlschema.rb | 2 | ||||
-rw-r--r-- | spec/ruby/library/time/xmlschema_spec.rb | 2 | ||||
-rw-r--r-- | test/ruby/test_time.rb | 56 | ||||
-rw-r--r-- | time.c | 82 |
9 files changed, 207 insertions, 3 deletions
diff --git a/benchmark/time_xmlschema.yml b/benchmark/time_xmlschema.yml new file mode 100644 index 0000000000..f746d9c6c8 --- /dev/null +++ b/benchmark/time_xmlschema.yml @@ -0,0 +1,23 @@ +prelude: | + # frozen_string_literal + unless Time.method_defined?(:xmlschema) + class Time + def xmlschema(fraction_digits=0) + fraction_digits = fraction_digits.to_i + s = strftime("%FT%T") + if fraction_digits > 0 + s << strftime(".%#{fraction_digits}N") + end + s << (utc? ? 'Z' : strftime("%:z")) + end + end + end + time = Time.now + utc_time = Time.now.utc +benchmark: + - time.xmlschema + - utc_time.xmlschema + - time.xmlschema(6) + - utc_time.xmlschema(6) + - time.xmlschema(9) + - utc_time.xmlschema(9) diff --git a/spec/ruby/core/time/iso8601_spec.rb b/spec/ruby/core/time/iso8601_spec.rb new file mode 100644 index 0000000000..ad60c3bb32 --- /dev/null +++ b/spec/ruby/core/time/iso8601_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/xmlschema' + +describe "Time#iso8601" do + it_behaves_like :time_xmlschema, :iso8601 +end diff --git a/spec/ruby/core/time/shared/xmlschema.rb b/spec/ruby/core/time/shared/xmlschema.rb new file mode 100644 index 0000000000..d68c18df36 --- /dev/null +++ b/spec/ruby/core/time/shared/xmlschema.rb @@ -0,0 +1,31 @@ +describe :time_xmlschema, shared: true do + ruby_version_is "3.4" do + it "generates ISO-8601 strings in Z for UTC times" do + t = Time.utc(1985, 4, 12, 23, 20, 50, 521245) + t.send(@method).should == "1985-04-12T23:20:50Z" + t.send(@method, 2).should == "1985-04-12T23:20:50.52Z" + t.send(@method, 9).should == "1985-04-12T23:20:50.521245000Z" + end + + it "generates ISO-8601 string with timeone offset for non-UTC times" do + t = Time.new(1985, 4, 12, 23, 20, 50, "+02:00") + t.send(@method).should == "1985-04-12T23:20:50+02:00" + t.send(@method, 2).should == "1985-04-12T23:20:50.00+02:00" + end + + it "year is always at least 4 digits" do + t = Time.utc(12, 4, 12) + t.send(@method).should == "0012-04-12T00:00:00Z" + end + + it "year can be more than 4 digits" do + t = Time.utc(40_000, 4, 12) + t.send(@method).should == "40000-04-12T00:00:00Z" + end + + it "year can be negative" do + t = Time.utc(-2000, 4, 12) + t.send(@method).should == "-2000-04-12T00:00:00Z" + end + end +end diff --git a/spec/ruby/core/time/xmlschema_spec.rb b/spec/ruby/core/time/xmlschema_spec.rb new file mode 100644 index 0000000000..bdf1dc7923 --- /dev/null +++ b/spec/ruby/core/time/xmlschema_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/xmlschema' + +describe "Time#xmlschema" do + it_behaves_like :time_xmlschema, :xmlschema +end diff --git a/spec/ruby/library/time/iso8601_spec.rb b/spec/ruby/library/time/iso8601_spec.rb index 4a9eb45613..9e2607fbd0 100644 --- a/spec/ruby/library/time/iso8601_spec.rb +++ b/spec/ruby/library/time/iso8601_spec.rb @@ -3,5 +3,5 @@ require_relative 'shared/xmlschema' require 'time' describe "Time.xmlschema" do - it_behaves_like :time_xmlschema, :iso8601 + it_behaves_like :time_library_xmlschema, :iso8601 end diff --git a/spec/ruby/library/time/shared/xmlschema.rb b/spec/ruby/library/time/shared/xmlschema.rb index 44d33cda7e..831d8509a7 100644 --- a/spec/ruby/library/time/shared/xmlschema.rb +++ b/spec/ruby/library/time/shared/xmlschema.rb @@ -1,4 +1,4 @@ -describe :time_xmlschema, shared: true do +describe :time_library_xmlschema, shared: true do it "parses ISO-8601 strings" do t = Time.utc(1985, 4, 12, 23, 20, 50, 520000) s = "1985-04-12T23:20:50.52Z" diff --git a/spec/ruby/library/time/xmlschema_spec.rb b/spec/ruby/library/time/xmlschema_spec.rb index 4279311199..ff3c864a02 100644 --- a/spec/ruby/library/time/xmlschema_spec.rb +++ b/spec/ruby/library/time/xmlschema_spec.rb @@ -3,5 +3,5 @@ require_relative 'shared/xmlschema' require 'time' describe "Time.xmlschema" do - it_behaves_like :time_xmlschema, :xmlschema + it_behaves_like :time_library_xmlschema, :xmlschema end diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index 2a541bbe8c..cab14eb694 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -1444,4 +1444,60 @@ class TestTime < Test::Unit::TestCase def test_parse_zero_bigint assert_equal 0, Time.new("2020-10-28T16:48:07.000Z").nsec, '[Bug #19390]' end + + def test_xmlschema_encode + [:xmlschema, :iso8601].each do |method| + bug6100 = '[ruby-core:42997]' + + t = Time.utc(2001, 4, 17, 19, 23, 17, 300000) + assert_equal("2001-04-17T19:23:17Z", t.__send__(method)) + assert_equal("2001-04-17T19:23:17.3Z", t.__send__(method, 1)) + assert_equal("2001-04-17T19:23:17.300000Z", t.__send__(method, 6)) + assert_equal("2001-04-17T19:23:17.3000000Z", t.__send__(method, 7)) + assert_equal("2001-04-17T19:23:17.3Z", t.__send__(method, 1.9), bug6100) + + t = Time.utc(2001, 4, 17, 19, 23, 17, 123456) + assert_equal("2001-04-17T19:23:17.1234560Z", t.__send__(method, 7)) + assert_equal("2001-04-17T19:23:17.123456Z", t.__send__(method, 6)) + assert_equal("2001-04-17T19:23:17.12345Z", t.__send__(method, 5)) + assert_equal("2001-04-17T19:23:17.1Z", t.__send__(method, 1)) + assert_equal("2001-04-17T19:23:17.1Z", t.__send__(method, 1.9), bug6100) + + t = Time.at(2.quo(3)).getlocal("+09:00") + assert_equal("1970-01-01T09:00:00.666+09:00", t.__send__(method, 3)) + assert_equal("1970-01-01T09:00:00.6666666666+09:00", t.__send__(method, 10)) + assert_equal("1970-01-01T09:00:00.66666666666666666666+09:00", t.__send__(method, 20)) + assert_equal("1970-01-01T09:00:00.6+09:00", t.__send__(method, 1.1), bug6100) + assert_equal("1970-01-01T09:00:00.666+09:00", t.__send__(method, 3.2), bug6100) + + t = Time.at(123456789.quo(9999999999)).getlocal("+09:00") + assert_equal("1970-01-01T09:00:00.012+09:00", t.__send__(method, 3)) + assert_equal("1970-01-01T09:00:00.012345678+09:00", t.__send__(method, 9)) + assert_equal("1970-01-01T09:00:00.0123456789+09:00", t.__send__(method, 10)) + assert_equal("1970-01-01T09:00:00.0123456789012345678+09:00", t.__send__(method, 19)) + assert_equal("1970-01-01T09:00:00.01234567890123456789+09:00", t.__send__(method, 20)) + assert_equal("1970-01-01T09:00:00.012+09:00", t.__send__(method, 3.8), bug6100) + + t = Time.utc(1) + assert_equal("0001-01-01T00:00:00Z", t.__send__(method)) + + begin + Time.at(-1) + rescue ArgumentError + # ignore + else + t = Time.utc(1960, 12, 31, 23, 0, 0, 123456) + assert_equal("1960-12-31T23:00:00.123456Z", t.__send__(method, 6)) + end + + assert_equal("10000-01-01T00:00:00Z", Time.utc(10000).__send__(method)) + assert_equal("9999-01-01T00:00:00Z", Time.utc(9999).__send__(method)) + assert_equal("0001-01-01T00:00:00Z", Time.utc(1).__send__(method)) # 1 AD + assert_equal("0000-01-01T00:00:00Z", Time.utc(0).__send__(method)) # 1 BC + assert_equal("-0001-01-01T00:00:00Z", Time.utc(-1).__send__(method)) # 2 BC + assert_equal("-0004-01-01T00:00:00Z", Time.utc(-4).__send__(method)) # 5 BC + assert_equal("-9999-01-01T00:00:00Z", Time.utc(-9999).__send__(method)) + assert_equal("-10000-01-01T00:00:00Z", Time.utc(-10000).__send__(method)) + end + end end @@ -5215,6 +5215,86 @@ time_strftime(VALUE time, VALUE format) } } +static VALUE +time_xmlschema(int argc, VALUE *argv, VALUE time) +{ + long fraction_digits = 0; + rb_check_arity(argc, 0, 1); + if (argc > 0) { + fraction_digits = NUM2LONG(argv[0]); + if (fraction_digits < 0) { + fraction_digits = 0; + } + } + + struct time_object *tobj; + + GetTimeval(time, tobj); + MAKE_TM(time, tobj); + + long year = -1; + if (FIXNUM_P(tobj->vtm.year)) { + year = FIX2LONG(tobj->vtm.year); + } + if (RB_UNLIKELY(year > 9999 || year < 0 || fraction_digits > 9)) { + // Slow path for uncommon dates. + VALUE format = rb_utf8_str_new_cstr("%FT%T"); + if (fraction_digits > 0) { + rb_str_catf(format, ".%%#%ldN", fraction_digits); + } + rb_str_cat_cstr(format, TZMODE_UTC_P(tobj) ? "Z" : "%:z"); + return rb_funcallv(time, rb_intern("strftime"), 1, &format); + } + + long buf_size = sizeof("YYYY-MM-DDTHH:MM:SS+ZH:ZM") + fraction_digits + (fraction_digits > 0 ? 1 : 0); + + VALUE str = rb_str_buf_new(buf_size); + rb_enc_associate_index(str, rb_utf8_encindex()); + + char *ptr = RSTRING_PTR(str); + char *start = ptr; + int written = snprintf( + ptr, + sizeof("YYYY-MM-DDTHH:MM:SS"), + "%04ld-%02d-%02dT%02d:%02d:%02d", + year, + tobj->vtm.mon, + tobj->vtm.mday, + tobj->vtm.hour, + tobj->vtm.min, + tobj->vtm.sec + ); + RUBY_ASSERT(written == sizeof("YYYY-MM-DDTHH:MM:SS") - 1); + ptr += written; + + if (fraction_digits > 0) { + long nsec = NUM2LONG(mulquov(tobj->vtm.subsecx, INT2FIX(1000000000), INT2FIX(TIME_SCALE))); + long subsec = nsec / (long)pow(10, 9 - fraction_digits); + + *ptr = '.'; + ptr++; + + written = snprintf(ptr, fraction_digits + 1, "%0*ld", (int)fraction_digits, subsec); // Always allow to write \0 + RUBY_ASSERT(written > 0); + ptr += written; + } + + if (TZMODE_UTC_P(tobj)) { + *ptr = 'Z'; + ptr++; + } + else { + long offset = NUM2LONG(rb_time_utc_offset(time)); + int offset_hours = (int)(offset / 3600); + int offset_minutes = (int)((offset % 3600 / 60)); + written = snprintf(ptr, sizeof("+ZH:ZM"), "%+03d:%02d", offset_hours, offset_minutes); + RUBY_ASSERT(written == sizeof("+ZH:ZM") - 1); + ptr += written; + } + rb_str_set_len(str, ptr -start); // We could skip coderange scanning as we know it's full ASCII. + return str; +} + int ruby_marshal_write_long(long x, char *buf); enum {base_dump_size = 8}; @@ -5842,6 +5922,8 @@ Init_Time(void) rb_define_method(rb_cTime, "subsec", time_subsec, 0); rb_define_method(rb_cTime, "strftime", time_strftime, 1); + rb_define_method(rb_cTime, "xmlschema", time_xmlschema, -1); + rb_define_alias(rb_cTime, "iso8601", "xmlschema"); /* methods for marshaling */ rb_define_private_method(rb_cTime, "_dump", time_dump, -1); |