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

Support timezone object argument to Time.{at,new} and Time#{getlocal,localtime} #3740

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -40,6 +40,7 @@ Compatibility:
* Add `Dir.for_fd` (#3681, @andrykonchin).
* Add `Dir.fchdir` (#3681, @andrykonchin).
* Add `Dir#chdir` (#3681, @andrykonchin).
* Support Timezone argument to `Time.{new,at}` and `Time#{getlocal,localtime}` (#1717, @patricklinpl, @manefz, @rwstauner).

Performance:

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 @@ -128,6 +128,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
47 changes: 24 additions & 23 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 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 @@ -54,4 +54,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
Expand Down
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/library/time/to_time_tags.txt

This file was deleted.

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