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

[GR-19220] Support timezone object argument to Time.{at,new} and Time#{getlocal,localtime} (#3740) #3786

Merged
merged 22 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a722c11
Support timezone object argument to Time.{at,new} and Time#{getlocal,…
rwstauner Feb 1, 2024
35a60fc
Ignore message content in TypeError assertion for Time#getlocal
rwstauner Feb 1, 2024
054754e
Update spec to show that when offset is numeric zone is nil
rwstauner Dec 12, 2024
8f3e304
Ensure zone object is a String before calling encoding methods
rwstauner Dec 12, 2024
087eaec
Support time zone args that define local_to_utc
rwstauner Dec 12, 2024
dbc9563
Set time zone to the object that did the conversion
rwstauner Dec 12, 2024
eeda7d4
Remove spec error message and just match on type
rwstauner Dec 12, 2024
3fcadc8
Make to_time available without needing to require date
rwstauner Dec 12, 2024
464aadb
Untag additional time specs that pass now
rwstauner Dec 12, 2024
49b1341
Fix coerce_to_exact_num and raise proper exception for objects that c…
andrykonchin Dec 23, 2024
7ff145c
Fix Time{.now,.at,#getlocal,#localtime} when #utc_to_local returns In…
andrykonchin Dec 24, 2024
c8c6550
Fix Time.now and set timezone object
andrykonchin Dec 24, 2024
696d17a
Fix Time{.now,.at,#getlocal,#localtime} when #utc_to_local returns in…
andrykonchin Dec 24, 2024
aa9f839
Polish specs for Time.new method
andrykonchin Dec 24, 2024
ddb8474
Untag passing specs for Time
andrykonchin Dec 24, 2024
be5db86
Refactor Truffle::Type.coerce_to_utc_offset
andrykonchin Dec 24, 2024
c45eecf
Cleanup calculate_utc_offset_with_timezone_object
eregon Feb 5, 2025
cda07c2
Move Time-related methods from Truffle::Type to Truffle::TimeOperations
andrykonchin Feb 6, 2025
0e34d77
Update specs for Time-like object and check only documented methods
andrykonchin Feb 6, 2025
e77028e
Test Time class public methods presence
andrykonchin Feb 6, 2025
5553611
Clean up Time#zone method and remove outdated encoding-specific logic
andrykonchin Feb 6, 2025
f4eea14
Refactor Truffle::Type.coerce_to_exact_num()
andrykonchin Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
4 changes: 2 additions & 2 deletions spec/ruby/core/time/at_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions spec/ruby/core/time/getlocal_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions spec/ruby/core/time/localtime_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 26 additions & 28 deletions spec/ruby/core/time/new_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -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
Expand All @@ -235,24 +234,20 @@ 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
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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down
91 changes: 91 additions & 0 deletions spec/ruby/core/time/now_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 0 additions & 2 deletions spec/tags/core/time/at_tags.txt

This file was deleted.

5 changes: 0 additions & 5 deletions spec/tags/core/time/getlocal_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion spec/tags/core/time/minus_tags.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
fails:Time#- maintains subseconds precision
fails:Time#- maintains precision
fails:Time#- zone is a timezone object preserves time zone
13 changes: 0 additions & 13 deletions spec/tags/core/time/new_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion spec/tags/core/time/now_tags.txt

This file was deleted.

1 change: 0 additions & 1 deletion spec/tags/core/time/plus_tags.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
fails:Time#+ maintains subseconds precision
fails:Time#+ zone is a timezone object preserves time zone
1 change: 0 additions & 1 deletion spec/tags/core/time/succ_tags.txt

This file was deleted.

1 change: 0 additions & 1 deletion spec/tags/library/time/to_time_tags.txt

This file was deleted.

55 changes: 55 additions & 0 deletions spec/truffle/methods/Time.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion spec/truffle/methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions src/main/java/org/truffleruby/core/time/TimeNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading
Loading