From 6c9f9f5c6118ae024b9ffad8e0cb7eda625e8b2d Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Thu, 30 Apr 2026 16:33:41 -0700 Subject: [PATCH 1/4] [ruby/time] Add Time#rfc3339 and Time.rfc3339 This allows easier transition of code using DateTime to using Time. Time#rfc3339 is an alias to #xmlschema and is just for consistency. Time.rfc3339 is similar to Time.xmlschema, but allows a space between the date and time, while Time.xmlschema does not (it requires a "T"). Introduce a Time._xmlschema private method so that Time.xmlschema and Time.rfc3339 can share most of the implementation. https://github.com/ruby/time/commit/9bca33976e --- lib/time.rb | 42 +++++++++++++++++++++++++++++++++++++++--- test/test_time.rb | 1 + 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/time.rb b/lib/time.rb index e6aab3fa5d444b..f4302fa7882c9c 100644 --- a/lib/time.rb +++ b/lib/time.rb @@ -624,13 +624,48 @@ def httpdate(date) # You must require 'time' to use this method. # def xmlschema(time) - if /\A\s* + pattern = /\A\s* (-?\d+)-(\d\d)-(\d\d) T (\d\d):(\d\d):(\d\d) (\.\d+)? (Z|[+-]\d\d(?::?\d\d)?)? - \s*\z/ix =~ time + \s*\z/ix + _xmlschema(pattern, time) + end + alias iso8601 xmlschema + + # + # Parses +time+ as a dateTime defined by RFC3339 and converts it to + # a Time object. + # + # ArgumentError is raised if +time+ is not compliant with the format or if + # the Time class cannot represent the specified time. + # + # See #xmlschema for more information on this format. + # + # require 'time' + # + # Time.rfc3339("2011-10-05T22:26:12-04:00") + # #=> 2011-10-05 22:26:12-04:00 + # + # You must require 'time' to use this method. + # + def rfc3339(time) + pattern = /\A\s* + (-?\d{4})-(\d\d)-(\d\d) + [T\s] + (\d\d):(\d\d):(\d\d) + (\.\d+)? + (Z|[+-]\d\d(?::?\d\d)?)? + \s*\z/ix + _xmlschema(pattern, time) + end + + private + + def _xmlschema(pattern, time) + if pattern =~ time year = $1.to_i mon = $2.to_i day = $3.to_i @@ -656,7 +691,7 @@ def xmlschema(time) raise ArgumentError.new("invalid xmlschema format: #{time.inspect}") end end - alias iso8601 xmlschema + end # class << self # @@ -731,4 +766,5 @@ def xmlschema(fraction_digits=0) end end alias iso8601 xmlschema unless method_defined?(:iso8601) + alias rfc3339 xmlschema end diff --git a/test/test_time.rb b/test/test_time.rb index 55964d02fc1e78..229d73a5eb6ab3 100644 --- a/test/test_time.rb +++ b/test/test_time.rb @@ -574,6 +574,7 @@ def test_huge_precision test = $1 define_method(test) {__send__(sub, :xmlschema)} define_method(test.sub(/xmlschema/, 'iso8601')) {__send__(sub, :iso8601)} + define_method(test.sub(/xmlschema/, 'rfc3339')) {__send__(sub, :rfc3339)} end def test_parse_with_various_object From 75aeb225b8558ff908ea78bf608dfbba09bdc2f9 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 18 Jul 2026 13:50:21 -0700 Subject: [PATCH 2/4] Also output mandatory only ISEQ in dumps Methods with mandatory only support only dumps the ISEQ of the case with all the arguments and not the mandatory only case. This commit changes it to also dump the mandatory only ISEQ. --- iseq.c | 4 ++++ test/ruby/test_iseq.rb | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/iseq.c b/iseq.c index 93e58db2e65287..f2db484f59f03e 100644 --- a/iseq.c +++ b/iseq.c @@ -3002,6 +3002,10 @@ rb_iseq_disasm_recursive(const rb_iseq_t *iseq, VALUE indent) n += rb_iseq_disasm_insn(str, code, n, iseq, child); } + if (body->mandatory_only_iseq) { + rb_ary_push(child, (VALUE)body->mandatory_only_iseq); + } + for (l = 0; l < RARRAY_LEN(child); l++) { VALUE isv = rb_ary_entry(child, l); if (done_iseq && st_is_member(done_iseq, (st_data_t)isv)) continue; diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb index 30931587f8b6e4..0a06d01b08a1d4 100644 --- a/test/ruby/test_iseq.rb +++ b/test/ruby/test_iseq.rb @@ -524,6 +524,12 @@ def test_anon_block_param_in_disasm assert_equal [:&], param_names end + def test_disasm_mandatory_only_overloaded + disasm = RubyVM::InstructionSequence.disasm(Time.method(:at)) + assert_include disasm, "" + assert_include disasm, "" + end + def strip_lineno(source) source.gsub(/^.*?: /, "") end From 77d0d22b59e175a3f5ad64e53c0eaa7aa11eafcd Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 22 Jun 2026 15:56:42 +0200 Subject: [PATCH 3/4] [ruby/time] Optimize `Time.xmlschema` by ~4x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is currentyl fairly slow as it rely on a Regexp match, which isn't very fast an allocates a lot. Since Ruby 3.2, `Time.new(iso_string)` is supported, and over 8x faster than `Time.xmlschema`. So if we use the regexp with `match?` to avoid the allocations, and then delegate to `.new`, we can make `Time.xmlschema` around 4 times faster: ``` ruby 4.0.5 (2026-05-20 revision https://github.com/ruby/time/commit/64336ffd0e) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- xmlschema 67.645k i/100ms Time.new 531.202k i/100ms opt 263.541k i/100ms Calculating ------------------------------------- xmlschema 680.426k (± 0.7%) i/s (1.47 μs/i) - 3.450M in 5.070197s Time.new 5.736M (± 0.9%) i/s (174.34 ns/i) - 28.685M in 5.000832s opt 2.779M (± 1.1%) i/s (359.78 ns/i) - 13.968M in 5.025252s Comparison: xmlschema: 680426.2 i/s Time.new: 5736027.1 i/s - 8.43x faster opt: 2779497.0 i/s - 4.08x faster ``` ```ruby require 'time' require 'benchmark/ips' class Time class << self def baseline(time) if /\A\s* (-?\d+)-(\d\d)-(\d\d) T (\d\d):(\d\d):(\d\d) (\.\d+)? (Z|[+-]\d\d(?::?\d\d)?)? \s*\z/ix =~ time year = $1.to_i mon = $2.to_i day = $3.to_i hour = $4.to_i min = $5.to_i sec = $6.to_i usec = 0 if $7 usec = Rational($7) * 1000000 end if $8 zone = $8 off = zone_offset(zone) year, mon, day, hour, min, sec = apply_offset(year, mon, day, hour, min, sec, off) t = self.utc(year, mon, day, hour, min, sec, usec) force_zone!(t, zone, off) t else self.local(year, mon, day, hour, min, sec, usec) end else raise ArgumentError.new("invalid xmlschema format: #{time.inspect}") end end def match_new(time) if /\A\s* (-?\d+)-(\d\d)-(\d\d) T (\d\d):(\d\d):(\d\d) (\.\d+)? (Z|[+-]\d\d(?::?\d\d)?)? \s*\z/ix.match?(time) new(time) else raise ArgumentError.new("invalid xmlschema format: #{time.inspect}") end end end end now = Time.now iso = now.iso8601(6).freeze p now p Time.match_new(iso) Benchmark.ips do |x| x.report('xmlschema') { Time.baseline(iso) } x.report('Time.new') { Time.new(iso) } x.report('opt') { Time.match_new(iso) } x.compare!(order: :baseline) end ``` https://github.com/ruby/time/commit/b8c50d236c --- lib/time.rb | 54 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/time.rb b/lib/time.rb index f4302fa7882c9c..dfb6daf7cc2e6e 100644 --- a/lib/time.rb +++ b/lib/time.rb @@ -664,31 +664,41 @@ def rfc3339(time) private - def _xmlschema(pattern, time) - if pattern =~ time - year = $1.to_i - mon = $2.to_i - day = $3.to_i - hour = $4.to_i - min = $5.to_i - sec = $6.to_i - usec = 0 - if $7 - usec = Rational($7) * 1000000 + if RUBY_VERSION >= "3.2" + def _xmlschema(pattern, time) + if pattern.match?(time) + new(time) + else + raise ArgumentError.new("invalid xmlschema format: #{time.inspect}") end - if $8 - zone = $8 - off = zone_offset(zone) - year, mon, day, hour, min, sec = - apply_offset(year, mon, day, hour, min, sec, off) - t = self.utc(year, mon, day, hour, min, sec, usec) - force_zone!(t, zone, off) - t + end + else + def _xmlschema(pattern, time) + if pattern =~ time + year = $1.to_i + mon = $2.to_i + day = $3.to_i + hour = $4.to_i + min = $5.to_i + sec = $6.to_i + usec = 0 + if $7 + usec = Rational($7) * 1000000 + end + if $8 + zone = $8 + off = zone_offset(zone) + year, mon, day, hour, min, sec = + apply_offset(year, mon, day, hour, min, sec, off) + t = self.utc(year, mon, day, hour, min, sec, usec) + force_zone!(t, zone, off) + t + else + self.local(year, mon, day, hour, min, sec, usec) + end else - self.local(year, mon, day, hour, min, sec, usec) + raise ArgumentError.new("invalid xmlschema format: #{time.inspect}") end - else - raise ArgumentError.new("invalid xmlschema format: #{time.inspect}") end end From 183c9f8f5c7367a65ff6b0574ba966ad158c1ad8 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Thu, 30 Apr 2026 16:53:02 -0700 Subject: [PATCH 4/4] [ruby/time] Allow setting a default time zone for Time.parse This is to allow for easier conversion of DateTime.parse code to Time.parse. DateTime.parse will assume UTC for parsed times without a time zone or offset, while Time will assume local time. This allows using `zone: "UTC"` to match the DateTime.parse behavior. Ideally, it would have pulled the timezone from the `now` argument, but I believe that backwards incompatibility from doing that now is too great. You can work around this now by appending " UTC" to the parsed string, as that is ignored if the string actually has a timezone or offset, but that doesn't seem like a clean approach. https://github.com/ruby/time/commit/62dc50dae5 --- lib/time.rb | 7 +++++-- test/test_time.rb | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/time.rb b/lib/time.rb index dfb6daf7cc2e6e..1c66088d33ac21 100644 --- a/lib/time.rb +++ b/lib/time.rb @@ -369,6 +369,9 @@ def make_time(date, year, yday, mon, day, hour, min, sec, sec_fraction, zone, no # If the extracted time zone abbreviation does not match any of them, # it is ignored and the given time is regarded as a local time. # + # A +zone+ argument can be provided to specify the zone for the given + # +date+, if the +date+ does not include a time zone or offset. + # # ArgumentError is raised if Date._parse cannot extract information from # +date+ or if the Time class cannot represent specified date. # @@ -382,12 +385,12 @@ def make_time(date, year, yday, mon, day, hour, min, sec, sec_fraction, zone, no # # You must require 'time' to use this method. # - def parse(date, now=self.now) + def parse(date, now=self.now, zone: nil) comp = !block_given? d = Date._parse(date, comp) year = d[:year] year = yield(year) if year && !comp - make_time(date, year, d[:yday], d[:mon], d[:mday], d[:hour], d[:min], d[:sec], d[:sec_fraction], d[:zone], now) + make_time(date, year, d[:yday], d[:mon], d[:mday], d[:hour], d[:min], d[:sec], d[:sec_fraction], d[:zone] || zone, now) end # diff --git a/test/test_time.rb b/test/test_time.rb index 229d73a5eb6ab3..a4df0b9ef4d9a1 100644 --- a/test/test_time.rb +++ b/test/test_time.rb @@ -370,6 +370,15 @@ def test_parse_offset_hour_minute_second assert_equal(t, Time.parse("1200-02-15 BC 14:13:20-00:00:00")) end + def test_parse_custom_offset + t = Time.at(-100000000000).utc + assert_equal(t, Time.parse("1200-02-15 BC 14:13:20", zone: "UTC")) + assert_equal(t, Time.parse("1200-02-15 BC 15:13:20", zone: "+01:00").utc) + + assert_equal(t, Time.parse("1200-02-15 BC 15:13:20+01:00", zone: "UTC").utc) + assert_equal(t, Time.parse("1200-02-15 BC 15:13:20+01:00", zone: "+02:00").utc) + end + def test_parse_leap_second t = Time.utc(1998,12,31,23,59,59) assert_equal(t, Time.parse("Thu Dec 31 23:59:59 UTC 1998"))