diff --git a/CHANGELOG.md b/CHANGELOG.md index fa123a20398a..70e39ff94839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Compatibility: * Fix `Module#remove_const` and emit warning when constant is deprecated (@andrykonchin). * Add `Module#set_temporary_name` (#3681, @andrykonchin). * Modify `Float#round` to match MRI behavior (#3676, @andrykonchin). +* Support Timezone argument to `Time.{new,at}` and `Time#{getlocal,localtime}` (#1717, @patricklinpl, @manefz, @rwstauner). Performance: diff --git a/spec/ruby/core/time/at_spec.rb b/spec/ruby/core/time/at_spec.rb index 85bb6d7ebf05..4b16e4131d26 100644 --- a/spec/ruby/core/time/at_spec.rb +++ b/spec/ruby/core/time/at_spec.rb @@ -102,8 +102,8 @@ it "needs for the argument to respond to #to_int too" do o = mock('rational-but-no-to_int') - o.should_receive(:to_r).and_return(Rational(5, 2)) - -> { Time.at(o) }.should raise_error(TypeError) + def o.to_r; Rational(5, 2) end + -> { Time.at(o) }.should raise_error(TypeError, "can't convert MockObject into an exact number") end end end diff --git a/spec/ruby/core/time/getlocal_spec.rb b/spec/ruby/core/time/getlocal_spec.rb index ff3e3d8dd1aa..d89e140caff0 100644 --- a/spec/ruby/core/time/getlocal_spec.rb +++ b/spec/ruby/core/time/getlocal_spec.rb @@ -14,6 +14,7 @@ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(3630) t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630) t.utc_offset.should == 3630 + t.zone.should be_nil end platform_is_not :windows do diff --git a/spec/ruby/core/time/localtime_spec.rb b/spec/ruby/core/time/localtime_spec.rb index 5badba9fb265..281be9ab17e9 100644 --- a/spec/ruby/core/time/localtime_spec.rb +++ b/spec/ruby/core/time/localtime_spec.rb @@ -188,6 +188,17 @@ end end + describe "with an argument that responds to #utc_to_local" do + it "coerces using #utc_to_local" do + o = mock('string') + o.should_receive(:utc_to_local).and_return(Time.new(2007, 1, 9, 13, 0, 0, 3600)) + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime(o) + t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600) + t.utc_offset.should == 3600 + end + end + it "raises ArgumentError if the String argument is not of the form (+|-)HH:MM" do t = Time.now -> { t.localtime("3600") }.should raise_error(ArgumentError) diff --git a/spec/ruby/core/time/new_spec.rb b/spec/ruby/core/time/new_spec.rb index 1a743b485ef3..59b13216ad9f 100644 --- a/spec/ruby/core/time/new_spec.rb +++ b/spec/ruby/core/time/new_spec.rb @@ -193,6 +193,7 @@ end end +# The method #local_to_utc is tested only here because Time.new is the only method that calls #local_to_utc. describe "Time.new with a timezone argument" do it "returns a Time in the timezone" do zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) @@ -213,9 +214,7 @@ def zone.local_to_utc(time) time end - -> { - Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) - }.should_not raise_error + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) end it "raises TypeError if timezone does not implement #local_to_utc method" do @@ -226,7 +225,7 @@ def zone.utc_to_local(time) -> { Time.new(2000, 1, 1, 12, 0, 0, zone) - }.should raise_error(TypeError, /can't convert \w+ into an exact number/) + }.should raise_error(TypeError, /can't convert Object into an exact number/) end it "does not raise exception if timezone does not implement #utc_to_local method" do @@ -235,13 +234,11 @@ def zone.local_to_utc(time) time end - -> { - Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) - }.should_not raise_error + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) end # The result also should be a Time or Time-like object (not necessary to be the same class) - # The zone of the result is just ignored + # or respond to #to_int method. The zone of the result is just ignored. describe "returned value by #utc_to_local and #local_to_utc methods" do it "could be Time instance" do zone = Object.new @@ -249,10 +246,8 @@ def zone.local_to_utc(t) Time.utc(t.year, t.mon, t.day, t.hour - 1, t.min, t.sec) end - -> { - Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) - Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 - }.should_not raise_error + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 end it "could be Time subclass instance" do @@ -261,25 +256,23 @@ def zone.local_to_utc(t) Class.new(Time).utc(t.year, t.mon, t.day, t.hour - 1, t.min, t.sec) end - -> { - Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) - Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 - }.should_not raise_error + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 end it "could be any object with #to_i method" do zone = Object.new def zone.local_to_utc(time) - Struct.new(:to_i).new(time.to_i - 60*60) + obj = Object.new + obj.singleton_class.define_method(:to_i) { time.to_i - 60*60 } + obj end - -> { - Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) - Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 - }.should_not raise_error + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 end - it "could have any #zone and #utc_offset because they are ignored" do + it "could have any #zone and #utc_offset because they are ignored if it isn't an instance of Time" do zone = Object.new def zone.local_to_utc(time) Struct.new(:to_i, :zone, :utc_offset).new(time.to_i, 'America/New_York', -5*60*60) @@ -293,7 +286,15 @@ def zone.local_to_utc(time) Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 0 end - it "leads to raising Argument error if difference between argument and result is too large" do + it "cannot have arbitrary #utc_offset if it is an instance of Time" do + zone = Object.new + def zone.local_to_utc(t) + Time.new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, 9*60*60) + end + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 9*60*60 + end + + it "raises ArgumentError if difference between argument and result is too large" do zone = Object.new def zone.local_to_utc(t) Time.utc(t.year, t.mon, t.day + 1, t.hour, t.min, t.sec) @@ -318,12 +319,9 @@ def zone.local_to_utc(t) end it "implements subset of Time methods" do + # List only methods that are explicitly documented. [ - :year, :mon, :month, :mday, :hour, :min, :sec, - :tv_sec, :tv_usec, :usec, :tv_nsec, :nsec, :subsec, - :to_i, :to_f, :to_r, :+, :-, - :isdst, :dst?, :zone, :gmtoff, :gmt_offset, :utc_offset, :utc?, :gmt?, - :to_s, :inspect, :to_a, :to_time, + :year, :mon, :mday, :hour, :min, :sec, :to_i, :isdst ].each do |name| @obj.respond_to?(name).should == true end diff --git a/spec/ruby/core/time/now_spec.rb b/spec/ruby/core/time/now_spec.rb index 7c2425411ac8..53cfa555f4f1 100644 --- a/spec/ruby/core/time/now_spec.rb +++ b/spec/ruby/core/time/now_spec.rb @@ -85,4 +85,95 @@ end end end + + ruby_version_is '3.1' do # https://bugs.ruby-lang.org/issues/17485 + describe "Timezone object" do + it "raises TypeError if timezone does not implement #utc_to_local method" do + zone = Object.new + def zone.local_to_utc(time) + time + end + + -> { + Time.now(in: zone) + }.should raise_error(TypeError, /can't convert Object into an exact number/) + end + + it "does not raise exception if timezone does not implement #local_to_utc method" do + zone = Object.new + def zone.utc_to_local(time) + time + end + + Time.now(in: zone).should be_kind_of(Time) + end + + # The result also should be a Time or Time-like object (not necessary to be the same class) + # or Integer. The zone of the result is just ignored. + describe "returned value by #utc_to_local and #local_to_utc methods" do + it "could be Time instance" do + zone = Object.new + def zone.utc_to_local(t) + Time.new(t.year, t.mon, t.day, t.hour + 1, t.min, t.sec, t.utc_offset) + end + + Time.now(in: zone).should be_kind_of(Time) + Time.now(in: zone).utc_offset.should == 3600 + end + + it "could be Time subclass instance" do + zone = Object.new + def zone.utc_to_local(t) + Class.new(Time).new(t.year, t.mon, t.day, t.hour + 1, t.min, t.sec, t.utc_offset) + end + + Time.now(in: zone).should be_kind_of(Time) + Time.now(in: zone).utc_offset.should == 3600 + end + + it "could be Integer" do + zone = Object.new + def zone.utc_to_local(time) + time.to_i + 60*60 + end + + Time.now(in: zone).should be_kind_of(Time) + Time.now(in: zone).utc_offset.should == 60*60 + end + + it "could have any #zone and #utc_offset because they are ignored" do + zone = Object.new + def zone.utc_to_local(t) + Struct.new(:year, :mon, :mday, :hour, :min, :sec, :isdst, :to_i, :zone, :utc_offset) + .new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.isdst, t.to_i, 'America/New_York', -5*60*60) + end + Time.now(in: zone).utc_offset.should == 0 + + zone = Object.new + def zone.utc_to_local(t) + Struct.new(:year, :mon, :mday, :hour, :min, :sec, :isdst, :to_i, :zone, :utc_offset) + .new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.isdst, t.to_i, 'Asia/Tokyo', 9*60*60) + end + Time.now(in: zone).utc_offset.should == 0 + + zone = Object.new + def zone.utc_to_local(t) + Time.new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, 9*60*60) + end + Time.now(in: zone).utc_offset.should == 0 + end + + it "raises ArgumentError if difference between argument and result is too large" do + zone = Object.new + def zone.utc_to_local(t) + Time.utc(t.year, t.mon, t.day - 1, t.hour, t.min, t.sec, t.utc_offset) + end + + -> { + Time.now(in: zone) + }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + end + end end diff --git a/spec/tags/core/time/at_tags.txt b/spec/tags/core/time/at_tags.txt deleted file mode 100644 index 64ff982a4433..000000000000 --- a/spec/tags/core/time/at_tags.txt +++ /dev/null @@ -1,2 +0,0 @@ -fails:Time.at :in keyword argument could be a timezone object -fails:Time.at passed non-Time, non-Numeric with an argument that responds to #to_r needs for the argument to respond to #to_int too diff --git a/spec/tags/core/time/getlocal_tags.txt b/spec/tags/core/time/getlocal_tags.txt index 86ed81235d89..4ceab4ca6df0 100644 --- a/spec/tags/core/time/getlocal_tags.txt +++ b/spec/tags/core/time/getlocal_tags.txt @@ -2,9 +2,4 @@ fails:Time#getlocal returns a Time with a UTC offset of the specified number of fails:Time#getlocal with an argument that responds to #to_r coerces using #to_r fails:Time#getlocal raises ArgumentError if the argument represents a value less than or equal to -86400 seconds fails:Time#getlocal raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds -fails:Time#getlocal with a timezone argument returns a Time in the timezone -fails:Time#getlocal with a timezone argument accepts timezone argument that must have #local_to_utc and #utc_to_local methods -fails:Time#getlocal with a timezone argument raises TypeError if timezone does not implement #utc_to_local method -fails:Time#getlocal with a timezone argument does not raise exception if timezone does not implement #local_to_utc method fails:Time#getlocal with a timezone argument subject's class implements .find_timezone method calls .find_timezone to build a time object if passed zone name as a timezone argument -fails:Time#getlocal with a timezone argument subject's class implements .find_timezone method does not call .find_timezone if passed any not string/numeric/timezone timezone argument diff --git a/spec/tags/core/time/minus_tags.txt b/spec/tags/core/time/minus_tags.txt index c11d2e657c3d..01b97b9532dd 100644 --- a/spec/tags/core/time/minus_tags.txt +++ b/spec/tags/core/time/minus_tags.txt @@ -1,3 +1,2 @@ fails:Time#- maintains subseconds precision fails:Time#- maintains precision -fails:Time#- zone is a timezone object preserves time zone diff --git a/spec/tags/core/time/new_tags.txt b/spec/tags/core/time/new_tags.txt index 04c1c547bdb5..3115becd304c 100644 --- a/spec/tags/core/time/new_tags.txt +++ b/spec/tags/core/time/new_tags.txt @@ -2,24 +2,11 @@ fails:Time.new with a utc_offset argument returns a Time with a UTC offset of th fails:Time.new with a utc_offset argument with an argument that responds to #to_r coerces using #to_r fails:Time.new with a utc_offset argument raises ArgumentError if the argument represents a value less than or equal to -86400 seconds fails:Time.new with a utc_offset argument raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds -fails:Time.new with a timezone argument returns a Time in the timezone -fails:Time.new with a timezone argument accepts timezone argument that must have #local_to_utc and #utc_to_local methods -fails:Time.new with a timezone argument raises TypeError if timezone does not implement #local_to_utc method -fails:Time.new with a timezone argument does not raise exception if timezone does not implement #utc_to_local method fails:Time.new with a timezone argument the #abbr method is used by '%Z' in #strftime -fails:Time.new with a timezone argument returned value by #utc_to_local and #local_to_utc methods could be Time instance -fails:Time.new with a timezone argument returned value by #utc_to_local and #local_to_utc methods could be Time subclass instance -fails:Time.new with a timezone argument returned value by #utc_to_local and #local_to_utc methods could be any object with #to_i method -fails:Time.new with a timezone argument returned value by #utc_to_local and #local_to_utc methods could have any #zone and #utc_offset because they are ignored -fails:Time.new with a timezone argument returned value by #utc_to_local and #local_to_utc methods leads to raising Argument error if difference between argument and result is too large -fails:Time.new with a timezone argument Time-like argument of #utc_to_local and #local_to_utc methods implements subset of Time methods -fails:Time.new with a timezone argument Time-like argument of #utc_to_local and #local_to_utc methods has attribute values the same as a Time object in UTC fails:Time.new with a timezone argument #name method uses the optional #name method for marshaling fails:Time.new with a timezone argument #name method cannot marshal Time if #name method isn't implemented fails:Time.new with a timezone argument subject's class implements .find_timezone method calls .find_timezone to build a time object at loading marshaled data fails:Time.new with a timezone argument subject's class implements .find_timezone method calls .find_timezone to build a time object if passed zone name as a timezone argument -fails:Time.new with a timezone argument subject's class implements .find_timezone method does not call .find_timezone if passed any not string/numeric/timezone timezone argument -fails:Time.new with a timezone argument :in keyword argument could be a timezone object fails:Time.new with a timezone argument Time.new with a String argument accepts precision keyword argument and truncates specified digits of sub-second part fails:Time.new with a timezone argument Time.new with a String argument converts precision keyword argument into Integer if is not nil fails:Time.new with a timezone argument Time.new with a String argument raise TypeError is can't convert precision keyword argument into Integer diff --git a/spec/tags/core/time/now_tags.txt b/spec/tags/core/time/now_tags.txt deleted file mode 100644 index 34db2116bc46..000000000000 --- a/spec/tags/core/time/now_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Time.now :in keyword argument could be a timezone object diff --git a/spec/tags/core/time/plus_tags.txt b/spec/tags/core/time/plus_tags.txt index 6c9cd9641b7b..2f8e51a7ea59 100644 --- a/spec/tags/core/time/plus_tags.txt +++ b/spec/tags/core/time/plus_tags.txt @@ -1,2 +1 @@ fails:Time#+ maintains subseconds precision -fails:Time#+ zone is a timezone object preserves time zone diff --git a/spec/tags/core/time/succ_tags.txt b/spec/tags/core/time/succ_tags.txt deleted file mode 100644 index fa0027c7aa93..000000000000 --- a/spec/tags/core/time/succ_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Time#succ zone is a timezone object preserves time zone diff --git a/spec/tags/library/time/to_time_tags.txt b/spec/tags/library/time/to_time_tags.txt deleted file mode 100644 index 9023a70bfa8a..000000000000 --- a/spec/tags/library/time/to_time_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Time#to_time returns itself in the same timezone diff --git a/spec/truffle/methods/Time.txt b/spec/truffle/methods/Time.txt new file mode 100644 index 000000000000..05e6982f08ec --- /dev/null +++ b/spec/truffle/methods/Time.txt @@ -0,0 +1,55 @@ ++ +- +<=> +asctime +ceil +ctime +day +deconstruct_keys +dst? +eql? +floor +friday? +getgm +getlocal +getutc +gmt? +gmt_offset +gmtime +gmtoff +hash +hour +inspect +isdst +localtime +mday +min +mon +monday? +month +nsec +round +saturday? +sec +strftime +subsec +sunday? +thursday? +to_a +to_f +to_i +to_r +to_s +tuesday? +tv_nsec +tv_sec +tv_usec +usec +utc +utc? +utc_offset +wday +wednesday? +yday +year +zone diff --git a/spec/truffle/methods_spec.rb b/spec/truffle/methods_spec.rb index 7ff07b4406dd..2795186ec7c4 100644 --- a/spec/truffle/methods_spec.rb +++ b/spec/truffle/methods_spec.rb @@ -27,7 +27,7 @@ Module Mutex NilClass Numeric Object ObjectSpace ObjectSpace::WeakKeyMap ObjectSpace::WeakMap Proc Process Process.singleton_class Queue Random Random::Formatter Random.singleton_class Range Rational Regexp Signal - SizedQueue String Struct Symbol SystemExit Thread TracePoint TrueClass + SizedQueue String Struct Symbol SystemExit Thread Time TracePoint TrueClass UnboundMethod Warning Digest Digest.singleton_class diff --git a/src/main/java/org/truffleruby/core/time/TimeNodes.java b/src/main/java/org/truffleruby/core/time/TimeNodes.java index 7559e4fabef8..4786546c6cb7 100644 --- a/src/main/java/org/truffleruby/core/time/TimeNodes.java +++ b/src/main/java/org/truffleruby/core/time/TimeNodes.java @@ -409,9 +409,8 @@ Object timeZone(RubyTime time) { @Primitive(name = "time_set_zone") public abstract static class TimeSetZoneNode extends PrimitiveArrayArgumentsNode { - @Specialization(guards = "strings.isRubyString(this, zone)", limit = "1") - Object timeSetZone(RubyTime time, Object zone, - @Cached RubyStringLibrary strings) { + @Specialization + Object timeSetZone(RubyTime time, Object zone) { time.zone = zone; return zone; } diff --git a/src/main/ruby/truffleruby/core/marshal.rb b/src/main/ruby/truffleruby/core/marshal.rb index c99c5646ac99..102468e774e1 100644 --- a/src/main/ruby/truffleruby/core/marshal.rb +++ b/src/main/ruby/truffleruby/core/marshal.rb @@ -140,7 +140,7 @@ class Exception end class Time - def __custom_marshal__(ms) + private def __custom_marshal__(ms) out = ''.b # Order matters. @@ -1307,8 +1307,8 @@ def serialize_user_class!(klass) end def serialize_user_defined(obj) - if Primitive.respond_to? obj, :__custom_marshal__, false - return obj.__custom_marshal__(self) + if Primitive.respond_to? obj, :__custom_marshal__, true + return obj.send(:__custom_marshal__, self) end str = obj.__send__ :_dump, @depth diff --git a/src/main/ruby/truffleruby/core/time.rb b/src/main/ruby/truffleruby/core/time.rb index f97ba37df6de..a9c4da240b83 100644 --- a/src/main/ruby/truffleruby/core/time.rb +++ b/src/main/ruby/truffleruby/core/time.rb @@ -107,13 +107,11 @@ def getlocal(offset = nil) def zone zone = Primitive.time_zone(self) - if zone && zone.ascii_only? - zone.encode Encoding::US_ASCII - elsif zone && Encoding.default_internal - zone.encode Encoding.default_internal - else - zone + if zone && Primitive.is_a?(zone, String) && zone.ascii_only? + return zone.encode Encoding::US_ASCII end + + zone end # Random number for hash codes. Stops hashes for similar values in @@ -166,10 +164,12 @@ def getgm end alias_method :getutc, :getgm - def localtime(offset = nil) + def localtime(zone_or_offset = nil) + offset = zone_or_offset if offset to_utc = Time.send(:utc_offset_in_utc?, offset) - offset = Truffle::Type.coerce_to_utc_offset(offset) + offset = Truffle::TimeOperations.calculate_utc_offset_with_timezone_object(offset, :utc_to_local, self) || + Truffle::TimeOperations.coerce_to_utc_offset(offset) end # the only cases when #localtime is allowed for a frozen time - @@ -183,16 +183,12 @@ def localtime(offset = nil) if to_utc Primitive.time_utctime(self) else - Primitive.time_localtime(self, offset) + result = Primitive.time_localtime(self, offset) + Truffle::TimeOperations.set_zone_if_object(result, zone_or_offset) + result end end - def succ - warn 'Time#succ is obsolete', uplevel: 1 - - self + 1 - end - def +(other) raise TypeError, 'time + time?' if Primitive.is_a?(other, Time) @@ -347,8 +343,6 @@ class << self def at(sec, sub_sec = undefined, unit = undefined, **kwargs) # **kwargs is used here because 'in' is a ruby keyword timezone = kwargs[:in] - offset = timezone ? Truffle::Type.coerce_to_utc_offset(timezone) : nil - is_utc = utc_offset_in_utc?(timezone) if offset result = if Primitive.undefined?(sub_sec) if Primitive.is_a?(sec, Time) @@ -362,10 +356,20 @@ def at(sec, sub_sec = undefined, unit = undefined, **kwargs) Primitive.time_at self, sec.to_i, ns end end + + if timezone + offset = Truffle::TimeOperations.calculate_utc_offset_with_timezone_object(timezone, :utc_to_local, result) || + Truffle::TimeOperations.coerce_to_utc_offset(timezone) + is_utc = utc_offset_in_utc?(timezone) + else + offset = nil + end + if result && offset result = is_utc ? Primitive.time_utctime(result) : Primitive.time_localtime(result, offset) end if result + Truffle::TimeOperations.set_zone_if_object(result, timezone) return result end @@ -426,9 +430,14 @@ def new(year = undefined, month = undefined, day = nil, hour = nil, minute = nil if utc_offset_in_utc?(utc_offset) utc_offset = :utc else - utc_offset = Truffle::Type.coerce_to_utc_offset(utc_offset) + zone = utc_offset + as_utc = Time.utc(year, month, day, hour, minute, second) + utc_offset = Truffle::TimeOperations.calculate_utc_offset_with_timezone_object(utc_offset, :local_to_utc, as_utc) || + Truffle::TimeOperations.coerce_to_utc_offset(utc_offset) end - Truffle::TimeOperations.compose(self, utc_offset, year, month, day, hour, minute, second) + result = Truffle::TimeOperations.compose(self, utc_offset, year, month, day, hour, minute, second) + Truffle::TimeOperations.set_zone_if_object(result, zone) + result end end @@ -442,9 +451,13 @@ def now(**options) in_timezone = options[:in] if in_timezone - utc_offset = Truffle::Type.coerce_to_utc_offset(in_timezone) + utc_offset = Truffle::TimeOperations.calculate_utc_offset_with_timezone_object(in_timezone, :utc_to_local, time_now) || + Truffle::TimeOperations.coerce_to_utc_offset(in_timezone) + is_utc = utc_offset_in_utc?(in_timezone) - is_utc ? Primitive.time_utctime(time_now) : Primitive.time_localtime(time_now, utc_offset) + time_in_timezone = is_utc ? Primitive.time_utctime(time_now) : Primitive.time_localtime(time_now, utc_offset) + Truffle::TimeOperations.set_zone_if_object(time_in_timezone, in_timezone) + time_in_timezone else time_now end diff --git a/src/main/ruby/truffleruby/core/truffle/time_operations.rb b/src/main/ruby/truffleruby/core/truffle/time_operations.rb index d4e9e9d12484..8df4ccb4996b 100644 --- a/src/main/ruby/truffleruby/core/truffle/time_operations.rb +++ b/src/main/ruby/truffleruby/core/truffle/time_operations.rb @@ -118,8 +118,93 @@ def self.utc_offset_for_compose(utc_offset) elsif Time.send(:utc_offset_in_utc?, utc_offset) :utc else - Truffle::Type.coerce_to_utc_offset(utc_offset) + coerce_to_utc_offset(utc_offset) end end + + # When a timezone object is used (via its utc_to_local or local_to_utc methods) + # the resulting time object gets its zone set to be the object + # (but this isn't done when the zone is just an integer offset or string abbreviation). + def self.set_zone_if_object(time, zone) + return if Primitive.nil?(zone) || Primitive.is_a?(zone, Integer) || Primitive.is_a?(zone, String) + + Primitive.time_set_zone(time, zone) + end + + def self.calculate_utc_offset_with_timezone_object(zone, conversion_method, time) + if conversion_method == :local_to_utc && Primitive.respond_to?(zone, :local_to_utc, false) + Primitive.assert time.utc? + as_utc = zone.local_to_utc(time) + offset = time.to_i - as_utc.to_i + elsif conversion_method == :utc_to_local && Primitive.respond_to?(zone, :utc_to_local, false) + time ||= Time.now + as_local = zone.utc_to_local(time.getutc) + offset = if Primitive.is_a?(as_local, Time) + as_local.to_i + as_local.utc_offset - time.to_i + else + as_local.to_i - time.to_i + end + else + return nil + end + + validate_utc_offset(offset) + offset + end + + def self.coerce_to_utc_offset(offset) + offset = String.try_convert(offset) || offset + + if Primitive.is_a? offset, String + offset = coerce_string_to_utc_offset(offset) + else + offset = Truffle::Type.coerce_to_exact_num(offset) + end + + if Primitive.is_a?(offset, Rational) + offset = offset.round + end + + validate_utc_offset(offset) + offset + end + + def self.validate_utc_offset(offset) + if offset <= -86400 || offset >= 86400 + raise ArgumentError, 'utc_offset out of range' + end + end + + UTC_OFFSET_WITH_COLONS_PATTERN = /\A(?\+|-)(?\d\d)(?::(?\d\d)(?::(?\d\d))?)?\z/ + UTC_OFFSET_WITHOUT_COLONS_PATTERN = /\A(?\+|-)(?\d\d)(?:(?\d\d)(?:(?\d\d))?)?\z/ + UTC_OFFSET_PATTERN = /#{UTC_OFFSET_WITH_COLONS_PATTERN}|#{UTC_OFFSET_WITHOUT_COLONS_PATTERN}/ + + def self.coerce_string_to_utc_offset(offset) + unless offset.encoding.ascii_compatible? + raise ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: ' + offset.inspect + end + + if offset == 'UTC' + offset = 0 + elsif offset.size == 1 && ('A'..'Z') === offset && offset != 'J' + if offset == 'Z' + offset = 0 + elsif offset < 'J' # skip J + offset = (offset.ord - 'A'.ord + 1) * 3600 # ("A".."I") => 1, 2, ... + elsif offset > 'J' && offset <= 'M' + offset = (offset.ord - 'A'.ord) * 3600 # ("K".."M") => 10, 11, 12 + else + offset = (offset.ord - 'N'.ord + 1) * -3600 # ("N"..Y) => -1, -2, ... + end + elsif (m = offset.match(UTC_OFFSET_PATTERN)) && m[:minutes].to_i < 60 && m[:seconds].to_i < 60 + # ignore hours - they are validated indirectly in #coerce_to_utc_offset + offset = m[:hours].to_i*60*60 + m[:minutes].to_i*60 + m[:seconds].to_i + offset = -offset if m[:sign] == '-' + else + raise ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: ' + offset + end + + offset + end end end diff --git a/src/main/ruby/truffleruby/core/type.rb b/src/main/ruby/truffleruby/core/type.rb index 433999aa776c..9c3704b479c9 100644 --- a/src/main/ruby/truffleruby/core/type.rb +++ b/src/main/ruby/truffleruby/core/type.rb @@ -468,71 +468,21 @@ def self.symbol_or_string_to_symbol(obj) end end - # Equivalent of num_exact in MRI's time.c, used by Time methods. + # MRI: num_exact() in time.c def self.coerce_to_exact_num(obj) if Primitive.is_a? obj, Integer obj - elsif Primitive.is_a? obj, String - raise TypeError, "can't convert #{obj} into an exact number" - elsif Primitive.nil? obj - raise TypeError, "can't convert nil into an exact number" + # MRI: test to_int method availability to reject non-Numeric objects such as String, + # Time, etc which have to_r method. + elsif Primitive.respond_to?(obj, :to_int, false) and rational = rb_check_convert_type(obj, Rational, :to_r) + rational + elsif integer = rb_check_convert_type(obj, Integer, :to_int) + integer else - rb_check_convert_type(obj, Rational, :to_r) || coerce_to(obj, Integer, :to_int) + raise TypeError, "can't convert #{Primitive.class(obj)} into an exact number" end end - def self.coerce_to_utc_offset(offset) - offset = String.try_convert(offset) || offset - - if Primitive.is_a? offset, String - offset = Truffle::Type.coerce_string_to_utc_offset(offset) - else - offset = Truffle::Type.coerce_to_exact_num(offset) - end - - if Primitive.is_a?(offset, Rational) - offset = offset.round - end - - if offset <= -86400 || offset >= 86400 - raise ArgumentError, 'utc_offset out of range' - end - - offset - end - - UTC_OFFSET_WITH_COLONS_PATTERN = /\A(?\+|-)(?\d\d)(?::(?\d\d)(?::(?\d\d))?)?\z/ - UTC_OFFSET_WITHOUT_COLONS_PATTERN = /\A(?\+|-)(?\d\d)(?:(?\d\d)(?:(?\d\d))?)?\z/ - UTC_OFFSET_PATTERN = /#{UTC_OFFSET_WITH_COLONS_PATTERN}|#{UTC_OFFSET_WITHOUT_COLONS_PATTERN}/ - - def self.coerce_string_to_utc_offset(offset) - unless offset.encoding.ascii_compatible? - raise ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: ' + offset.inspect - end - - if offset == 'UTC' - offset = 0 - elsif offset.size == 1 && ('A'..'Z') === offset && offset != 'J' - if offset == 'Z' - offset = 0 - elsif offset < 'J' # skip J - offset = (offset.ord - 'A'.ord + 1) * 3600 # ("A".."I") => 1, 2, ... - elsif offset > 'J' && offset <= 'M' - offset = (offset.ord - 'A'.ord) * 3600 # ("K".."M") => 10, 11, 12 - else - offset = (offset.ord - 'N'.ord + 1) * -3600 # ("N"..Y) => -1, -2, ... - end - elsif (m = offset.match(UTC_OFFSET_PATTERN)) && m[:minutes].to_i < 60 && m[:seconds].to_i < 60 - # ignore hours - they are validated indirectly in #coerce_to_utc_offset - offset = m[:hours].to_i*60*60 + m[:minutes].to_i*60 + m[:seconds].to_i - offset = -offset if m[:sign] == '-' - else - raise ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: ' + offset - end - - offset - end - def self.coerce_to_bitwise_operand(obj) if Primitive.is_a? obj, Float raise TypeError, "can't convert Float into Integer for bitwise arithmetic"