
Issue #20951 has been updated by nagachika (Tomoyuki Chikanaga). Backport changed from 3.1: REQUIRED, 3.2: REQUIRED, 3.3: REQUIRED to 3.1: REQUIRED, 3.2: REQUIRED, 3.3: DONE ruby_3_3 commit:56ba9041d9e338359b32ba0bfb3d816d57dc9d39 merged revision(s) commit:ae6bd3b49ba252985b92416c24102ede3c0aac9b, commit:966458199d870b88b42898d4a063b487c1ef6b00, commit:966458199d870b88b42898d4a063b487c1ef6b00. ---------------------------------------- Bug #20951: Confusing handling of timezone object's `#utc_to_local` results https://bugs.ruby-lang.org/issues/20951#change-112220 * Author: andrykonchin (Andrew Konchin) * Status: Closed * Backport: 3.1: REQUIRED, 3.2: REQUIRED, 3.3: DONE ---------------------------------------- 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`: ```ruby 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. ```ruby 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: ```ruby 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: ```ruby 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. ```ruby 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. -- https://bugs.ruby-lang.org/