[ruby-core:120232] [Ruby master Misc#20951] Confusing handling of timezone object's `#utc_to_local` results

Issue #20951 has been reported by andrykonchin (Andrew Konchin). ---------------------------------------- Misc #20951: Confusing handling of timezone object's `#utc_to_local` results https://bugs.ruby-lang.org/issues/20951 * Author: andrykonchin (Andrew Konchin) * Status: Open ---------------------------------------- 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/

Issue #20951 has been updated by nobu (Nobuyoshi Nakada). 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`:
```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 ```
Really? `#to_i` is different on my machine. ```ruby 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 in `sec`/`min`/`hours` values otherwise.
I'll update the documentation to note that `#to_i` is used to represent the UTC offset. ---------------------------------------- Misc #20951: Confusing handling of timezone object's `#utc_to_local` results https://bugs.ruby-lang.org/issues/20951#change-111007 * Author: andrykonchin (Andrew Konchin) * Status: Feedback ---------------------------------------- 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/

Issue #20951 has been updated by nobu (Nobuyoshi Nakada). 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. ---------------------------------------- Bug #20951: Confusing handling of timezone object's `#utc_to_local` results https://bugs.ruby-lang.org/issues/20951#change-111016 * Author: andrykonchin (Andrew Konchin) * Status: Feedback * Backport: 3.1: REQUIRED, 3.2: REQUIRED, 3.3: REQUIRED ---------------------------------------- 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/

Issue #20951 has been updated by andrykonchin (Andrew Konchin). 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. ---------------------------------------- Bug #20951: Confusing handling of timezone object's `#utc_to_local` results https://bugs.ruby-lang.org/issues/20951#change-111019 * Author: andrykonchin (Andrew Konchin) * Status: Feedback * Backport: 3.1: REQUIRED, 3.2: REQUIRED, 3.3: REQUIRED ---------------------------------------- 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/

Issue #20951 has been updated by nagachika (Tomoyuki Chikanaga). Status changed from Feedback to Closed Moved to "Closed" status to trigger backport. ---------------------------------------- Bug #20951: Confusing handling of timezone object's `#utc_to_local` results https://bugs.ruby-lang.org/issues/20951#change-112219 * Author: andrykonchin (Andrew Konchin) * Status: Closed * Backport: 3.1: REQUIRED, 3.2: REQUIRED, 3.3: REQUIRED ---------------------------------------- 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/

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/

Issue #20951 has been updated by hsbt (Hiroshi SHIBATA). Backport changed from 3.1: REQUIRED, 3.2: REQUIRED, 3.3: DONE to 3.1: DONTNEED, 3.2: DONTNEED, 3.3: DONE, 3.4: DONTNEED `doc/_timezones.rdoc` is introduced from Ruby 3.3 release. I removed Ruby 3.1 and 3.2 from backport targets. ---------------------------------------- Bug #20951: Confusing handling of timezone object's `#utc_to_local` results https://bugs.ruby-lang.org/issues/20951#change-112269 * Author: andrykonchin (Andrew Konchin) * Status: Closed * Backport: 3.1: DONTNEED, 3.2: DONTNEED, 3.3: DONE, 3.4: DONTNEED ---------------------------------------- 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/
participants (4)
-
andrykonchin (Andrew Konchin)
-
hsbt (Hiroshi SHIBATA)
-
nagachika (Tomoyuki Chikanaga)
-
nobu (Nobuyoshi Nakada)