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.
Updated by nobu (Nobuyoshi Nakada) 8 days ago
- Status changed from Open to Feedback
andrykonchin (Andrew Konchin) wrote:
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
Really?
#to_i
is different on my machine.
require 'tzinfo'
p TZInfo::VERSION #=> "2.0.6"
zone = TZInfo::Timezone.get("Europe/Kiev")
t = 1734107333
time = Time.at(t, in: zone)
p zone.utc_to_local(time).then{|u|[u.to_i, u.to_i==t, t]} #=> [1734114533, false, 1734107333]
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 insec
/min
/hours
values otherwise.
I'll update the documentation to note that #to_i
is used to represent the UTC offset.
Updated by nobu (Nobuyoshi Nakada) 7 days ago
- Tracker changed from Misc to Bug
- Backport set to 3.1: REQUIRED, 3.2: REQUIRED, 3.3: REQUIRED
Moved to Bug to back port the documentation update.
Updated by andrykonchin (Andrew Konchin) 7 days ago
Thank you!
I've spotted the difference in the examples above. In my example the utc_to_local
's argument is converted to UTC (time = Time.now.utc
) and in your example it's in the "Europe/Kiev"
timezone.
I made assumption that the #utc_to_local
method accepts a time-like object in UTC.
So it seems there is gap in the documentation because logic of #utc_to_local
and probably #local_to_utc
methods isn't obvious and clear from reading the description.