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

Implementing correct offsets for to_local #52

Merged
merged 9 commits into from Sep 19, 2016
52 changes: 45 additions & 7 deletions lib/tzinfo/time_or_datetime.rb
Expand Up @@ -11,7 +11,7 @@ class TimeOrDateTime
# Constructs a new TimeOrDateTime. timeOrDateTime can be a Time, DateTime
# or Integer. If using a Time or DateTime, any time zone information
# is ignored.
def initialize(timeOrDateTime)
def initialize(timeOrDateTime, ignore_offset = true)
@time = nil
@datetime = nil
@timestamp = nil
Expand All @@ -23,11 +23,11 @@ def initialize(timeOrDateTime)
nsec = @time.nsec
usec = nsec % 1000 == 0 ? nsec / 1000 : Rational(nsec, 1000)

@time = Time.utc(@time.year, @time.mon, @time.mday, @time.hour, @time.min, @time.sec, usec) unless @time.utc?
@time = Time.utc(@time.year, @time.mon, @time.mday, @time.hour, @time.min, @time.sec, usec) unless @time.utc? || !ignore_offset
@orig = @time
elsif timeOrDateTime.is_a?(DateTime)
@datetime = timeOrDateTime
@datetime = @datetime.new_offset(0) unless @datetime.offset == 0
@datetime = @datetime.new_offset(0) unless @datetime.offset == 0 || !ignore_offset
@orig = @datetime
else
@timestamp = timeOrDateTime.to_i
Expand All @@ -49,7 +49,10 @@ def to_time
if @timestamp
@time = Time.at(@timestamp).utc
else
@time = Time.utc(year, mon, mday, hour, min, sec, usec)
# Avoid using Rational unless necessary.
u = usec
s = u == 0 ? sec : Rational(sec * 1000000 + u, 1000000)
@time = Time.new(year, mon, mday, hour, min, s, offset)
end
end

Expand All @@ -70,7 +73,7 @@ def to_datetime
# Avoid using Rational unless necessary.
u = usec
s = u == 0 ? sec : Rational(sec * 1000000 + u, 1000000)
@datetime = DateTime.new(year, mon, mday, hour, min, s)
@datetime = DateTime.new(year, mon, mday, hour, min, s, OffsetRationals.rational_for_offset(offset))
end

@datetime
Expand Down Expand Up @@ -195,6 +198,18 @@ def usec
end
end

# Returns utc offset of original value _in seconds_ (or 0 if original
# value was integer timestamp).
def offset
if @time
@time.utc_offset
elsif @datetime
(3600*24*@datetime.offset).to_i
else
0
end
end

# Compares this TimeOrDateTime with another Time, DateTime, timestamp
# (Integer) or TimeOrDateTime. Returns -1, 0 or +1 depending
# whether the receiver is less than, equal to, or greater than
Expand Down Expand Up @@ -255,6 +270,23 @@ def -(seconds)
self + (-seconds)
end

# Converts TimeOrDateTime to new UTC offset.
# Considers original value's UTC offset wisely.
def to_offset(seconds)
if @orig.is_a?(DateTime)
off = OffsetRationals.rational_for_offset(seconds)
TimeOrDateTime.new(@orig.new_offset(off), false)
elsif @orig.is_a?(Time)
time = @time.getutc + seconds
nsec_part = Rational(time.nsec, 1_000_000_000)
time = Time.new(time.year, time.mon, time.mday, time.hour, time.min, time.sec + nsec_part, seconds)
TimeOrDateTime.new(time, false)
else
# Integer: fallback to "just shift timestamp"
TimeOrDateTime.new(@orig + seconds)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaves slightly differently to the Time and DateTime cases, which feels a bit suspect to me.

end
end

# Returns true if todt represents the same time and was originally
# constructed with the same type (DateTime, Time or timestamp) as this
# TimeOrDateTime.
Expand All @@ -278,8 +310,14 @@ def hash
# TimeOrDateTime. If a TimeOrDateTime is passed in, no new TimeOrDateTime
# will be constructed and the value passed to wrap will be used when
# calling the block.
def self.wrap(timeOrDateTime)
t = timeOrDateTime.is_a?(TimeOrDateTime) ? timeOrDateTime : TimeOrDateTime.new(timeOrDateTime)
#
# Optional ignore_offset second parameter (defaults to true) controls
# whether timezone/UTC offset of input value will be considered or
# ignored completely (in a latter case `2016-06-01 12:30:50 +03:00`
# and `2016-06-01 12:30:50 GMT` would be wrapped into exactly the
# same `TimeOrDateTime` object).
def self.wrap(timeOrDateTime, ignore_offset = true)
t = timeOrDateTime.is_a?(TimeOrDateTime) ? timeOrDateTime : TimeOrDateTime.new(timeOrDateTime, ignore_offset)

if block_given?
t = yield t
Expand Down
20 changes: 20 additions & 0 deletions lib/tzinfo/timezone.rb
Expand Up @@ -418,6 +418,16 @@ def period_for_local(local, dst = Timezone.default_dst)
end
end

# Returns the TimezonePeriod for the given local time. local can either be
# a DateTime, Time or integer timestamp (Time.to_i). Unlike `period_for_local`,
# actually considers timezone information of local, thus eliminating
# all complexities about AmbiguousTime.
#
def period_for(local)
# FIXME: maybe define TimeOrDateTime#to_utc as a synonym for #to_offset(0)?..
period_for_utc(TimeOrDateTime.wrap(local, false).to_offset(0))
end

# Converts a time in UTC to the local timezone. utc can either be
# a DateTime, Time or timestamp (Time.to_i). The returned time has the same
# type as utc. Any timezone information in utc is ignored (it is treated as
Expand All @@ -428,6 +438,16 @@ def utc_to_local(utc)
}
end

# Converts a time in some local timezone to another local timezone.
# another_local can either be a DateTime, Time or timestamp (Time.to_i).
#
# Considers timezone of another_local.
def to_local(another_local)
TimeOrDateTime.wrap(another_local, false) {|wrapped|
period_for(wrapped).to_local(wrapped)
}
end

# Converts a time in the local timezone to UTC. local can either be
# a DateTime, Time or timestamp (Time.to_i). The returned time has the same
# type as local. Any timezone information in local is ignored (it is treated
Expand Down
2 changes: 1 addition & 1 deletion lib/tzinfo/timezone_offset.rb
Expand Up @@ -36,7 +36,7 @@ def dst?
# the offset of this period.
def to_local(utc)
TimeOrDateTime.wrap(utc) {|wrapped|
wrapped + @utc_total_offset
wrapped.to_offset(@utc_total_offset)
}
end

Expand Down
63 changes: 63 additions & 0 deletions test/tc_time_or_datetime.rb
Expand Up @@ -18,6 +18,15 @@ def test_initialize_time_local
assert(tdt.to_orig.utc?)
end

def test_initialize_time_local_preserve_offset
t = Time.new(2006, 3, 24, 15, 32, 3, '+03:00')
tdt = TimeOrDateTime.new(t, false)
assert_equal(t, tdt.to_time)
assert_equal(t, tdt.to_orig)
assert(!tdt.to_time.utc?)
assert(!tdt.to_orig.utc?)
end

def test_intialize_time_local_usec
tdt = TimeOrDateTime.new(Time.local(2006, 3, 24, 15, 32, 3, 721123))
assert_equal(Time.utc(2006, 3, 24, 15, 32, 3, 721123), tdt.to_time)
Expand Down Expand Up @@ -94,6 +103,17 @@ def test_to_time
TimeOrDateTime.new('1143214323').to_time)
end

def test_to_time_preserve_offset
assert_equal(Time.new(2006, 3, 24, 15, 32, 3, '+03:00'),
TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3, '+03:00'), false).to_time)
assert_equal(Time.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+03:00'),
TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+03:00'), false).to_time)
assert_equal(Time.new(2006, 3, 24, 15, 32, 3, '+03:00'),
TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3, '+3'), false).to_time)
assert_equal(Time.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+03:00'),
TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+3'), false).to_time)
end

def test_to_time_trunc_to_usec
assert_equal(Time.utc(2006, 3, 24, 15, 32, 3, 721123),
TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(7211239, 10000000))).to_time)
Expand All @@ -114,6 +134,17 @@ def test_to_datetime
TimeOrDateTime.new('1143214323').to_datetime)
end

def test_to_datetime_preserve_offset
assert_equal(DateTime.new(2006, 3, 24, 15, 32, 3, '+3'),
TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3, '+03:00'), false).to_datetime)
assert_equal(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+3'),
TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+03:00'), false).to_datetime)
assert_equal(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+3'),
TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(721123, 1000000), '+3'), false).to_datetime)
assert_equal(DateTime.new(2006, 3, 24, 15, 32, 3, '+3'),
TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3, '+3'), false).to_datetime)
end

def test_to_datetime_ruby186_bug
# DateTime.new in Ruby 1.8.6 won't allow a time to be specified using
# fractions of a second that is within the 60th second of a minute.
Expand Down Expand Up @@ -225,6 +256,18 @@ def test_usec
assert_equal(0, TimeOrDateTime.new(1143214323).usec)
end

def test_offset_ignored
assert_equal(0, TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3, '+03:00')).offset)
assert_equal(0, TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3, '+03:00')).offset)
assert_equal(0, TimeOrDateTime.new(1143214323).offset)
end

def test_offset_preserved
assert_equal(10800, TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3, '+03:00'), false).offset)
assert_equal(10800, TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3, '+03:00'), false).offset)
assert_equal(0, TimeOrDateTime.new(1143214323, false).offset)
end

def test_usec_after_to_i
val = TimeOrDateTime.new(Time.utc(2013, 2, 4, 22, 10, 33, 598000))
assert_equal(Time.utc(2013, 2, 4, 22, 10, 33).to_i, val.to_i)
Expand Down Expand Up @@ -482,6 +525,26 @@ def test_subtract
assert_equal(1143214324, (TimeOrDateTime.new(1143214323) - (-1)).to_orig)
end

def test_to_offset
assert_equal(Time.utc(2006, 3, 24, 15, 32, 3), (TimeOrDateTime.new(Time.utc(2006, 3, 24, 15, 32, 3)).to_offset(0)).to_orig)
assert_equal(Time.utc(2006, 3, 24, 15, 32, 3, 721000), (TimeOrDateTime.new(Time.utc(2006, 3, 24, 15, 32, 3, 721000)).to_offset(0)).to_orig)
assert_equal(DateTime.new(2006, 3, 24, 15, 32, 3), (TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3)).to_offset(0)).to_orig)
assert_equal(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(721, 1000)), (TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(721, 1000))).to_offset(0)).to_orig)
assert_equal(1143214323, (TimeOrDateTime.new(1143214323).to_offset(0)).to_orig)

assert_equal(Time.new(2006, 3, 24, 18, 32, 3, '+03:00'), (TimeOrDateTime.new(Time.utc(2006, 3, 24, 15, 32, 3)).to_offset(3600*3)).to_orig)
assert_equal(Time.new(2006, 3, 24, 18, 32, 3 + Rational(721, 1000), '+03:00'), (TimeOrDateTime.new(Time.utc(2006, 3, 24, 15, 32, 3, 721000)).to_offset(3600*3)).to_orig)
assert_equal(DateTime.new(2006, 3, 24, 18, 32, 3, '+3'), (TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3)).to_offset(3600*3)).to_orig)
assert_equal(DateTime.new(2006, 3, 24, 18, 32, 3 + Rational(721, 1000), '+3'), (TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3 + Rational(721, 1000))).to_offset(3600*3)).to_orig)
assert_equal(1143225123, (TimeOrDateTime.new(1143214323).to_offset(3600*3)).to_orig)

assert_equal(Time.new(2006, 3, 24, 16, 32, 3, '+03:00'), (TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3, '+02:00'), false).to_offset(3600*3)).to_orig)
assert_equal(DateTime.new(2006, 3, 24, 16, 32, 3, '+3'), (TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3, '+2'), false).to_offset(3600*3)).to_orig)

assert_equal(Time.utc(2006, 3, 24, 13, 32, 3), (TimeOrDateTime.new(Time.new(2006, 3, 24, 15, 32, 3, '+02:00'), false).to_offset(3600*3)).to_orig)
assert_equal(DateTime.new(2006, 3, 24, 13, 32, 3), (TimeOrDateTime.new(DateTime.new(2006, 3, 24, 15, 32, 3, '+2'), false).to_offset(3600*3)).to_orig)
end

def test_wrap_time
t = TimeOrDateTime.wrap(Time.utc(2006, 3, 24, 15, 32, 3))
assert_instance_of(TimeOrDateTime, t)
Expand Down
94 changes: 74 additions & 20 deletions test/tc_timezone.rb
Expand Up @@ -706,6 +706,31 @@ def test_period_for_local_block_ambiguous
end
end

def test_period_for
dt = DateTime.new(2005,2,18,16,24,23,'+03:00')
t = Time.new(2005,2,18,16,24,23,'+03:00')
ts = t.to_i

dt_utc = TimeOrDateTime.wrap(dt, false).to_offset(0)
t_utc = TimeOrDateTime.wrap(t, false).to_offset(0)
ts_utc = TimeOrDateTime.wrap(ts, false).to_offset(0)

o1 = TimezoneOffset.new(0, 0, :GMT)
o2 = TimezoneOffset.new(0, 3600, :BST)

period = TimezonePeriod.new(
TestTimezoneTransition.new(o1, o2, 1099184400),
TestTimezoneTransition.new(o2, o1, 1111885200))

dt_period = TestTimezone.new('Europe/London', period, nil, dt_utc).period_for(dt)
t_period = TestTimezone.new('Europe/London', period, nil, t_utc).period_for(t)
ts_period = TestTimezone.new('Europe/London', period, nil, ts_utc).period_for(ts)

assert_equal(period, dt_period)
assert_equal(period, t_period)
assert_equal(period, ts_period)
end

def test_utc_to_local
dt = DateTime.new(2005,6,18,16,24,23)
dt2 = DateTime.new(2005,6,18,16,24,23).new_offset(Rational(5,24))
Expand All @@ -724,14 +749,14 @@ def test_utc_to_local
TestTimezoneTransition.new(o2, o1, 1111885200),
TestTimezoneTransition.new(o1, o2, 1130634000))

assert_equal(DateTime.new(2005,6,18,17,24,23), TestTimezone.new('Europe/London', period, [], dt).utc_to_local(dt))
assert_equal(DateTime.new(2005,6,18,17,24,23), TestTimezone.new('Europe/London', period, [], dt2).utc_to_local(dt2))
assert_equal(DateTime.new(2005,6,18,17,24,23 + Rational(567,1000)), TestTimezone.new('Europe/London', period, [], dtu).utc_to_local(dtu))
assert_equal(DateTime.new(2005,6,18,17,24,23 + Rational(567,1000)), TestTimezone.new('Europe/London', period, [], dtu2).utc_to_local(dtu2))
assert_equal(Time.utc(2005,6,18,17,24,23), TestTimezone.new('Europe/London', period, [], t).utc_to_local(t))
assert_equal(Time.utc(2005,6,18,17,24,23), TestTimezone.new('Europe/London', period, [], t2).utc_to_local(t2))
assert_equal(Time.utc(2005,6,18,17,24,23,567000), TestTimezone.new('Europe/London', period, [], tu).utc_to_local(tu))
assert_equal(Time.utc(2005,6,18,17,24,23,567000), TestTimezone.new('Europe/London', period, [], tu2).utc_to_local(tu2))
assert_equal(DateTime.new(2005,6,18,17,24,23,'+1'), TestTimezone.new('Europe/London', period, [], dt).utc_to_local(dt))
assert_equal(DateTime.new(2005,6,18,17,24,23,'+1'), TestTimezone.new('Europe/London', period, [], dt2).utc_to_local(dt2))
assert_equal(DateTime.new(2005,6,18,17,24,23 + Rational(567,1000),'+1'), TestTimezone.new('Europe/London', period, [], dtu).utc_to_local(dtu))
assert_equal(DateTime.new(2005,6,18,17,24,23 + Rational(567,1000),'+1'), TestTimezone.new('Europe/London', period, [], dtu2).utc_to_local(dtu2))
assert_equal(Time.new(2005,6,18,17,24,23,'+01:00'), TestTimezone.new('Europe/London', period, [], t).utc_to_local(t))
assert_equal(Time.new(2005,6,18,17,24,23,'+01:00'), TestTimezone.new('Europe/London', period, [], t2).utc_to_local(t2))
assert_equal(Time.new(2005,6,18,17,24,23+Rational(567,1000), '+01:00'), TestTimezone.new('Europe/London', period, [], tu).utc_to_local(tu))
assert_equal(Time.new(2005,6,18,17,24,23+Rational(567,1000), '+01:00'), TestTimezone.new('Europe/London', period, [], tu2).utc_to_local(tu2))
assert_equal(Time.utc(2005,6,18,17,24,23).to_i, TestTimezone.new('Europe/London', period, [], ts).utc_to_local(ts))
end

Expand All @@ -752,18 +777,47 @@ def test_utc_to_local_offset
TestTimezoneTransition.new(o2, o1, 1111885200),
TestTimezoneTransition.new(o1, o2, 1130634000))

assert_equal(0, TestTimezone.new('Europe/London', period, [], dt).utc_to_local(dt).offset)
assert_equal(0, TestTimezone.new('Europe/London', period, [], dt2).utc_to_local(dt2).offset)
assert_equal(0, TestTimezone.new('Europe/London', period, [], dtu).utc_to_local(dtu).offset)
assert_equal(0, TestTimezone.new('Europe/London', period, [], dtu2).utc_to_local(dtu2).offset)
assert_equal(0, TestTimezone.new('Europe/London', period, [], t).utc_to_local(t).utc_offset)
assert(TestTimezone.new('Europe/London', period, [], t).utc_to_local(t).utc?)
assert_equal(0, TestTimezone.new('Europe/London', period, [], t2).utc_to_local(t2).utc_offset)
assert(TestTimezone.new('Europe/London', period, [], t2).utc_to_local(t2).utc?)
assert_equal(0, TestTimezone.new('Europe/London', period, [], tu).utc_to_local(tu).utc_offset)
assert(TestTimezone.new('Europe/London', period, [], tu).utc_to_local(tu).utc?)
assert_equal(0, TestTimezone.new('Europe/London', period, [], tu2).utc_to_local(tu2).utc_offset)
assert(TestTimezone.new('Europe/London', period, [], tu2).utc_to_local(tu2).utc?)
assert_equal(Rational(1, 24), TestTimezone.new('Europe/London', period, [], dt).utc_to_local(dt).offset)
assert_equal(Rational(1, 24), TestTimezone.new('Europe/London', period, [], dt2).utc_to_local(dt2).offset)
assert_equal(Rational(1, 24), TestTimezone.new('Europe/London', period, [], dtu).utc_to_local(dtu).offset)
assert_equal(Rational(1, 24), TestTimezone.new('Europe/London', period, [], dtu2).utc_to_local(dtu2).offset)
assert_equal(3600, TestTimezone.new('Europe/London', period, [], t).utc_to_local(t).utc_offset)
assert(!TestTimezone.new('Europe/London', period, [], t).utc_to_local(t).utc?)
assert_equal(3600, TestTimezone.new('Europe/London', period, [], t2).utc_to_local(t2).utc_offset)
assert(!TestTimezone.new('Europe/London', period, [], t2).utc_to_local(t2).utc?)
assert_equal(3600, TestTimezone.new('Europe/London', period, [], tu).utc_to_local(tu).utc_offset)
assert(!TestTimezone.new('Europe/London', period, [], tu).utc_to_local(tu).utc?)
assert_equal(3600, TestTimezone.new('Europe/London', period, [], tu2).utc_to_local(tu2).utc_offset)
assert(!TestTimezone.new('Europe/London', period, [], tu2).utc_to_local(tu2).utc?)
end

def test_to_local
dt = DateTime.new(2005,6,18,16,24,23)
dt2 = DateTime.new(2005,6,18,16,24,23, '+3')
dtu = DateTime.new(2005,6,18,16,24,23 + Rational(567,1000))
dtu2 = DateTime.new(2005,6,18,16,24,23 + Rational(567,1000), '+3')
t = Time.utc(2005,6,18,16,24,23)
t2 = Time.new(2005,6,18,16,24,23, '+03:00')
tu = Time.utc(2005,6,18,16,24,23,567000)
tu2 = Time.new(2005,6,18,16,24,23 + Rational(567, 1000), '+03:00')
ts = t.to_i

o1 = TimezoneOffset.new(0, 0, :GMT)
o2 = TimezoneOffset.new(0, 3600, :BST)

period = TimezonePeriod.new(
TestTimezoneTransition.new(o2, o1, 1111885200),
TestTimezoneTransition.new(o1, o2, 1130634000))

assert_equal(DateTime.new(2005,6,18,17,24,23,'+1'), TestTimezone.new('Europe/London', period, [], dt).to_local(dt))
assert_equal(DateTime.new(2005,6,18,14,24,23,'+1'), TestTimezone.new('Europe/London', period, [], dt2).to_local(dt2))
assert_equal(DateTime.new(2005,6,18,17,24,23 + Rational(567,1000),'+1'), TestTimezone.new('Europe/London', period, [], dtu).to_local(dtu))
assert_equal(DateTime.new(2005,6,18,14,24,23 + Rational(567,1000),'+1'), TestTimezone.new('Europe/London', period, [], dtu2).to_local(dtu2))
assert_equal(Time.new(2005,6,18,17,24,23,'+01:00'), TestTimezone.new('Europe/London', period, [], t).to_local(t))
assert_equal(Time.new(2005,6,18,14,24,23,'+01:00'), TestTimezone.new('Europe/London', period, [], t2.getutc).to_local(t2))
assert_equal(Time.new(2005,6,18,17,24,23+Rational(567,1000), '+01:00'), TestTimezone.new('Europe/London', period, [], tu).to_local(tu))
assert_equal(Time.new(2005,6,18,14,24,23+Rational(567,1000), '+01:00'), TestTimezone.new('Europe/London', period, [], tu2.getutc).to_local(tu2))
assert_equal(Time.utc(2005,6,18,17,24,23).to_i, TestTimezone.new('Europe/London', period, [], ts).utc_to_local(ts))
end

def test_local_to_utc
Expand Down