Project

General

Profile

Actions

Bug #20951

closed

Confusing handling of timezone object's `#utc_to_local` results

Added by andrykonchin (Andrew Konchin) 9 days ago. Updated 7 days ago.

Status:
Feedback
Assignee:
-
Target version:
-
[ruby-core:120232]

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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0