New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Time#change
and #advance
around the end of DST
#46251
Fix Time#change
and #advance
around the end of DST
#46251
Conversation
07c6361
to
693fcbf
Compare
Prior to this commit, when `Time#change` or `Time#advance` constructed a time inside the final stretch of Daylight Saving Time (DST), the non-DST offset would always be chosen for local times: ```ruby # DST ended just before 2021-11-07 2:00:00 AM in US/Eastern. ENV["TZ"] = "US/Eastern" time = Time.local(2021, 11, 07, 00, 59, 59) + 1 # => 2021-11-07 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0500 time = Time.local(2021, 11, 06, 01, 00, 00) # => 2021-11-06 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(days: 1) # => 2021-11-07 01:00:00 -0500 ``` And the DST offset would always be chosen for times with a `TimeZone` object: ```ruby Time.zone = "US/Eastern" time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600 # => 2021-11-07 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0400 time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone) # => 2021-11-08 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(days: -1) # => 2021-11-07 01:00:00 -0400 ``` This commit fixes `Time#change` and `Time#advance` to choose the offset that matches the original time's offset when possible: ```ruby ENV["TZ"] = "US/Eastern" time = Time.local(2021, 11, 07, 00, 59, 59) + 1 # => 2021-11-07 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0400 time = Time.local(2021, 11, 06, 01, 00, 00) # => 2021-11-06 01:00:00 -0400 time.change(day: 07) # => 2021-11-07 01:00:00 -0400 time.advance(days: 1) # => 2021-11-07 01:00:00 -0400 Time.zone = "US/Eastern" time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600 # => 2021-11-07 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(seconds: 0) # => 2021-11-07 01:00:00 -0500 time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone) # => 2021-11-08 01:00:00 -0500 time.change(day: 07) # => 2021-11-07 01:00:00 -0500 time.advance(days: -1) # => 2021-11-07 01:00:00 -0500 ``` It's worth noting that there is a somewhat dubious case when calling `advance` with a mix of calendar and clock units (e.g. months + hours). `advance` adds calendar units first, then adds clock units. So a non-DST time may first be advanced to a date within DST before any clock units are applied. For example: ```ruby # DST began on 2021-03-14 in US/Eastern. Time.zone = "US/Eastern" non_dst = Time.new(2021, 03, 07, 00, 00, 00, Time.zone) # => 2021-03-07 00:00:00 -0500 non_dst.advance(months: 8, hours: 1) # => 2021-11-07 01:00:00 -0400 ``` One could argue that the expected result is `2021-11-07 01:00:00 -0500`. However, the difference between that and the result for `hours: 0` is greater than 1 hour: ```ruby adjusted_result = non_dst.advance(months: 8, hours: 1) + 3600 # => 2021-11-07 01:00:00 -0500 adjusted_result - non_dst.advance(months: 8, hours: 0) # => 7200.0 ``` Which might also be unexpected. Furthermore, it may be difficult (or expensive) to apply such an adjustment in a consistent way. For example, the result for `hours: 2` does have the expected `-0500` offset, so it might seem no adjustment is necessary, but if we didn't adjust it too, it would conflict with the adjusted `hours: 1` result: ```ruby non_dst.advance(months: 8, hours: 2) # => 2021-11-07 01:00:00 -0500 ``` Therefore, the approach in this commit (which produces a `-0400` offset for `hours: 1`) seems like a reasonable compromise. Fixes rails#45055. Closes rails#45556. Closes rails#46248. Co-authored-by: Kevin Hall <bigtoe416@yahoo.com> Co-authored-by: Takayoshi Nishida <takayoshi.nishida@gmail.com>
693fcbf
to
b191a65
Compare
There is a somewhat dubious case when calling # DST began on 2021-03-14 and ended just before 2021-11-07 2:00:00 AM in US/Eastern.
Time.zone = "US/Eastern"
non_dst = Time.new(2021, 03, 07, 00, 00, 00, Time.zone)
# => 2021-03-07 00:00:00 -0500
non_dst.advance(months: 8, hours: 1)
# => 2021-11-07 01:00:00 -0400 One could argue that the expected result is adjusted_result = non_dst.advance(months: 8, hours: 1) + 3600
# => 2021-11-07 01:00:00 -0500
adjusted_result - non_dst.advance(months: 8, hours: 0)
# => 7200.0 Which might also be unexpected. Furthermore, it may be difficult (or expensive) to apply such an adjustment in a consistent way. For example, the result for non_dst.advance(months: 8, hours: 2)
# => 2021-11-07 01:00:00 -0500 Therefore, I think the approach in this PR (which produces a |
Prior to this commit, when
Time#change
orTime#advance
constructed a time inside the final stretch of Daylight Saving Time (DST), the non-DST offset would always be chosen for local times:And the DST offset would always be chosen for times with a
TimeZone
object:This commit fixes
Time#change
andTime#advance
to choose the offset that matches the original time's offset when possible:Fixes #45055.
Closes #45556.
Closes #46248.
/cc @khall I've added you as co-author for your work on #46248.
/cc @takp I've added you as co-author for your work on #45556.