SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: ZonedDateTime.of silently moves a time in the DST gap an hour later

· by Serguey Asael Shinder / Serguey Shinder

Ireland and the UK changed their clocks last Sunday, which is a good moment to remember what java.time does with local times around a transition. It does not throw. It picks an answer for you.

Take the spring change in Dublin: on 29 March 2026 the clocks went from 01:00 straight to 02:00. A scheduled job stored as "01:30 local" has no valid instant on that day. Ask for it anyway:

ZoneId dublin = ZoneId.of("Europe/Dublin");
ZonedDateTime t = ZonedDateTime.of(
        LocalDateTime.of(2026, 3, 29, 1, 30), dublin);
System.out.println(t);   // 2026-03-29T02:30+01:00[Europe/Dublin]
Serguey Asael Shinder: ZonedDateTime.of silently moves a time in the DST gap an hour later
ZonedDateTime.of silently moves a time in the DST gap an hour later — Serguey Asael Shinder

The javadoc of ZonedDateTime.of describes this exactly: "In the case of a gap, when clocks jump forward, there is no valid offset. Instead, the local date-time is adjusted to be later by the length of the gap. For a typical one hour daylight savings change, the local date-time will be moved one hour later". So the job runs at 02:30, not at 01:30 and not at 02:00. If your code compares the resulting toLocalTime() with what was configured, they no longer match.

The autumn change has the opposite problem. Last Sunday, local times between 01:00 and 02:00 occurred twice. The same javadoc: "In the case of an overlap, when clocks are set back, there are two valid offsets. This method uses the earlier offset typically corresponding to 'summer'." A job at "01:30 local" therefore resolves to the first 01:30, in summer time, and nothing tells you there was a second one an hour later. If you need the other instant, withLaterOffsetAtOverlap() gives it to you.

None of this is a bug; it is a reasonable policy. The problem is that it is a policy applied silently to data that may have been wrong. Two habits help. Store instants, or a LocalDateTime plus an explicit rule for gaps and overlaps, for anything that must run exactly once. And when a local time comes from a user, check it against ZoneRules.getValidOffsets(localDateTime): an empty list means it falls in a gap, two entries mean it is ambiguous, and either is worth asking about rather than resolving quietly.

Source: java.time.ZonedDateTime javadoc, Java SE 25 — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/ZonedDateTime.html