A local appointment such as 02:30 does not always identify one instant. A daylight-saving transition can skip that time or repeat it with two different offsets. A scheduling form should make that ambiguity visible instead of silently choosing a result.
This example uses an explicit policy: accept a local date and time only when the selected zone supplies exactly one valid offset. Reject both gaps and overlaps, then let the caller request a different time or an explicit choice. The tests use historical Berlin transitions so they do not depend on today's date.
Prerequisites
Use JDK 21 with javac and java available. Save the following program as StrictTime.java. It needs no external libraries or preview features.
import java.time.*;
public class StrictTime {
static ZonedDateTime resolve(String value, ZoneId zone) {
var local = LocalDateTime.parse(value);
var offsets = zone.getRules().getValidOffsets(local);
if (offsets.size() != 1) throw new IllegalArgumentException("ambiguous or missing time");
return ZonedDateTime.ofStrict(local, offsets.get(0), zone);
}
public static void main(String[] args) {
var zone = ZoneId.of("Europe/Berlin");
var normal = resolve("2024-01-15T10:00", zone);
if (!normal.toInstant().equals(Instant.parse("2024-01-15T09:00:00Z")))
throw new AssertionError("wrong instant");
System.out.println(normal.toInstant());
for (var value : new String[]{"2024-03-31T02:30", "2024-10-27T02:30"}) {
try {
resolve(value, zone);
throw new AssertionError("accepted transition time");
} catch (IllegalArgumentException expected) {
System.out.println("rejected " + value);
}
}
}
}
Compile, run and compare
javac StrictTime.java
java StrictTime
2024-01-15T09:00:00Z
rejected 2024-03-31T02:30
rejected 2024-10-27T02:30
Understand the resolution policy
ZoneRules.getValidOffsets returns the offsets applicable to a local date and time. The resolver accepts only a list of size one and passes its offset to ZonedDateTime.ofStrict. A winter appointment resolves to 09:00 UTC. The spring and autumn fixtures exercise the two rejected transition cases.
Calling LocalDateTime.atZone directly uses Java's documented resolution behavior, including adjusting a gap and selecting an offset during an overlap. That behavior can be useful, but the strict appointment policy here deliberately asks the application to resolve exceptional inputs first.
Edge cases and troubleshooting
Keep the region identifier, such as Europe/Berlin, rather than substituting a fixed numeric offset for all future appointments. An offset alone does not carry the region's transition rules. Preserve the user's requested local time and zone when the business meaning is a future local appointment; also record the chosen instant when the booking is finalized.
If compilation fails, check that the filename matches the public class name and that javac comes from the intended JDK. A malformed date raises a parsing exception; a bad zone identifier fails before this resolver runs. An application should translate these failures into separate form errors instead of presenting the shared transition message for every input problem.
The included checks cover one ordinary time, one gap and one overlap. They do not verify recurring schedules or future legal changes to time zones. Keep the deployed JDK's time-zone data updated and define how existing future bookings should be handled when rules change.
Primary references
The Java 21 references for ZoneRules and ZonedDateTime document these APIs and resolution rules. Browse more Java tutorials.