Skip to content

Commit

Permalink
Return local times with offsets (#52)
Browse files Browse the repository at this point in the history
Resolves #49.
  • Loading branch information
zverok authored and philr committed Sep 19, 2016
1 parent 87fc615 commit 85ffbda
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 62 deletions.
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)
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

0 comments on commit 85ffbda

Please sign in to comment.