Bug #20951
closedConfusing handling of timezone object's `#utc_to_local` results
Description
I am looking into the timezone object feature (that is supported by various Time class methods) now and I am confused by the current implementation. Specifically, how a time-like object that is not inherited from the Time class is handled. A time-like object is returned for instance from the timezone object's #utc_to_local
method.
The documentation states that:
A Time-like object is a container object capable of interfacing with timezone libraries for timezone conversion.
Also
The zone value may be an object responding to certain timezone methods, an instance of Timezone and TZInfo for example.
And indeed the TZInfo::Timezone
class works as expected.
But when I try to use for time-like objects a brand new class not inherited from Time - it works incorrectly. Let's consider an example with TZInfo::Timezone
:
require 'tzinfo'
zone = TZInfo::Timezone.get("Europe/Kiev") # UTC+2
time = Time.now.utc
puts time.to_i # 1734107333
puts Time.now(in: zone) # 2024-12-13 18:28:53 +0200
puts zone.utc_to_local(time) # 2024-12-13 18:28:53 +0200
puts zone.utc_to_local(time).to_i # 1734107333
And now an example with a brand new class.
I make an assumption, that as far as zone.utc_to_local(time).to_i
doesn't change Unix timestamp (it equals time.to_i
, that's 1734107333), so in a new class also #utc_to_local
should return not modified value too.
TimeObj = Struct.new(:year, :mon, :mday, :hour, :min, :sec, :isdst, :to_i)
zone_obj = Object.new
def zone_obj.utc_to_local(t)
TimeObj.new(t.year, t.mon, t.mday, t.hour + 2, t.min, t.sec, t.isdst, t.to_i) # <=== adjust hours (`hours + 2`) to match "Europe/Kiev" timezone (that's UTC+2)
end
Unfortunately it produces incorrect result:
puts Time.now(in: zone_obj) # 2024-12-13 18:28:53 +0000 <====== wrong UTC offset
puts zone_obj.utc_to_local(time) # #<struct TimeObj year=2024, mon=12, mday=13, hour=18, min=28, sec=53, isdst=false, to_i=1734107333>
puts zone_obj.utc_to_local(time).to_i # 1734107333 <===== the same Unix timestamp
So now result time object has wrong utc offset - +0000
instead of +0200
.
Okey, so probably Unix timestamp should be adjusted as well. Let's check:
def zone_obj.utc_to_local(t)
TimeObj.new(t.year, t.mon, t.mday, t.hour + 2, t.min, t.sec, t.isdst, t.to_i + 2 * 60 * 60) # <===== adjust #to_i as well so it returns timestamp + 2 hours
end
puts Time.now(in: zone_obj) # 2024-12-13 18:28:53 +0200 <======= correct UTC offset
puts zone_obj.utc_to_local(time) # #<struct TimeObj year=2024, mon=12, mday=13, hour=18, min=28, sec=53, isdst=false, to_i=1734114533>
puts zone_obj.utc_to_local(time).to_i # 1734114533 <====== different Unix timestamp
Now we have correct UTC offset +0200
despite zone_obj.utc_to_local(time).to_i
returns not original offset but an adjusted one.
I assume the difference is caused by a special treatment of time-like object inherited from the Time class. So its utc_offset
property is used only. But for all the other classes the #to_i
is used instead.
zone.utc_to_local(time).class.ancestors
# => [TZInfo::TimeWithOffset, TZInfo::WithOffset, Time, Comparable, Object, PP::ObjectMixin, Kernel, BasicObject]
This difference is confusing so I think it makes sense either to document it (I mean to document that #to_i
should return adjusted value for non-related to Time classes) in case it's intentional or to change behaviour for non-related to Time classes and rely not on #to_i
to calculate UTC offset but on difference in sec
/min
/hours
values otherwise.