Skip to content

Keep the UTC offset in Time#change for times with a zone object - #59617

Open
thomasklemm wants to merge 1 commit into
rails:mainfrom
thomasklemm:thomasklemm/time-change-zone-offset
Open

thomasklemm wants to merge 1 commit into
rails:mainfrom
thomasklemm:thomasklemm/time-change-zone-offset

Conversation

@thomasklemm

@thomasklemm thomasklemm commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

On a Time whose zone is an object (TZInfo::Timezone or ActiveSupport::TimeZone), change couldn't stay in the hour that repeats when clocks go back. TZInfo zones raised TZInfo::AmbiguousTime, also from Active Model's datetime cast when it truncates precision. ActiveSupport::TimeZone could jump to the other occurrence of that hour.

change now tries the time's own UTC offset first and keeps it when the zone agrees. Otherwise the zone decides, as before.

At 00:30 UTC on 2026-10-25, both zones are in the first of two identical local hours:

Zone Start change(sec: 1) on main With this PR
Berlin, TZInfo::Timezone 02:30:00 +0200 raises AmbiguousTime 02:30:01 +0200
Dublin, ActiveSupport::TimeZone 01:30:00 +0100 01:30:01 +0000, an hour later 01:30:01 +0100

Still unchanged: an ambiguous time raises when neither occurrence has the original offset, and a time in the skipped spring hour raises PeriodNotFound.

Compared with main for every TZInfo and ActiveSupport::TimeZone zone around each transition up to 2037, results differ only where main raised or picked the other occurrence, and the tests cover both clock changes.

Time#change built the new time with Time.new(..., zone) and then moved
it to the second occurrence of a repeated hour when that matched the
original offset. For a TZInfo::Timezone with TZInfo's default
ambiguity settings, Time.new raises TZInfo::AmbiguousTime for a local
time in the repeated hour, so change(nsec:), change(hour:) and Active
Model's datetime cast, when precision truncation is needed, raised
there. Where the zone picks the other occurrence, as
ActiveSupport::TimeZone does for Europe/Dublin (whose winter time
counts as DST), the original offset was missed and change returned a
time one hour later.

It now tries the original UTC offset first and keeps it when the zone
gives that offset for the new local time, which is the occurrence
change already meant to pick, and builds the time from the zone as
before otherwise. When neither occurrence of the new local time has
the original offset, the zone picks as before, so a TZInfo zone still
raises there; a time in a skipped hour still raises as before.
@thomasklemm
thomasklemm force-pushed the thomasklemm/time-change-zone-offset branch from d5252fa to 553f926 Compare November 4, 2026 14:29

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant