As explained in @Misha's answer , this is due to daylight saving rules.
In Sao Paulo, the DST starts at midnight 2015-10-18: the clock moves forward 1 hour, so it “skips” from 23:59:59 to 01:00:00 . There is a space between 00:00:00 and 00:59:59 , so the time 00:30 adjusted accordingly.
You can check if the date and time for the time zone ZoneRules using the ZoneRules and ZoneOffsetTransition :
ZoneId sp = ZoneId.of("America/Sao_Paulo"); ZoneRules rules = sp.getRules(); // check if 2015-10-18 00:30 is valid for this timezone LocalDateTime dt = LocalDateTime.of(2015, 10, 18, 0, 30); List<ZoneOffset> validOffsets = rules.getValidOffsets(dt); System.out.println(validOffsets.size()); // size is zero, no valid offsets at 00:30
The getValidOffsets method returns all valid offsets for the specified date / time. If the list is empty, it means that the date / time does not exist in this time zone (usually due to the DST, the clock moves forward).
When the date / time exists in the time zone, the offset is returned:
ZoneId la = ZoneId.of("America/Los_Angeles"); rules = la.getRules(); validOffsets = rules.getValidOffsets(dt); System.out.println(validOffsets.size()); // 1 - date/time valid for this timezone System.out.println(validOffsets.get(0)); // -07:00
In Los_Angeles time zone, 1 valid offset is returned: -07:00 .
PS: Offset changes usually occur due to DST, but this is not always the case. DST and biases are determined by governments and laws, and they can change at any time. Thus, a gap in the allowable bias can also mean that such a change has occurred (some politician decided to change the standard bias of the country, so the gap may not necessarily be related to the DST).
You can also check when the change occurs, and what the offset is before and after it:
ZoneId sp = ZoneId.of("America/Sao_Paulo"); ZoneRules rules = sp.getRules();
Output:
Transition [Gap on 2015-10-18T00: 00-03: 00 to -02: 00]
This means that the interval (the clock moves forward) is 2015-10-18T00:00 , and the offset will change from -03:00 to -02:00 (so that the clock moves 1 hour ahead).
You can also get all this information separately:
System.out.println(t.getDateTimeBefore() + " -> " + t.getDateTimeAfter()); System.out.println(t.getOffsetBefore() + " -> " + t.getOffsetAfter());
Output:
2015-10-18T00: 00 → 2015-10-18T01: 00
-03: 00 → -02: 00
This shows that at 00:00 clock moves directly to 01:00 (therefore, 00:30 cannot exist). The second line is the offset before and after the change.
If you check the transitions in the Los_Angeles time zone, you will see that its DST starts and ends on different dates:
ZoneId la = ZoneId.of("America/Los_Angeles"); rules = la.getRules(); // 2015-10-18 00:30 in Los Angeles Instant instant = dt.atZone(la).toInstant(); System.out.println(rules.previousTransition(instant)); System.out.println(rules.nextTransition(instant));
Output:
Transition [gap in 2015-03-08T02: 00-08: 00 to -07: 00]
Transition [Overlap on 2015-11-01T02: 00-07: 00 to -08: 00]
So, in the Los_Angeles time zone, DST starts on 2015-03-08 and ends on 2015-11-01 . Therefore, at 2015-10-18 all hours are valid (there is no adjustment, as happens in the Sao_Paulo time zone).
Some time zones have transition rules (for example, "DST starts on the third Sunday of October") instead of transitions (for example, "DST starts with this specific date and time"), and you can also use them if available:
ZoneId sp = ZoneId.of("America/Sao_Paulo"); ZoneRules rules = sp.getRules();
Another way to check if the date and time is valid for some time zone is to use the ZonedDateTime.ofStrict method, which throws an exception if the date and time is not valid for the time zone:
ZoneId sp = ZoneId.of("America/Sao_Paulo"); ZoneId la = ZoneId.of("America/Los_Angeles"); LocalDateTime dt = LocalDateTime.of(2015, 10, 18, 0, 30); System.out.println(ZonedDateTime.ofStrict(dt, ZoneOffset.ofHours(-7), la)); // OK System.out.println(ZonedDateTime.ofStrict(dt, ZoneOffset.ofHours(-3), sp)); // throws java.time.DateTimeException
The first case is normal, because the offset of -7 is valid for Los Angeles on a given date / time. The second case throws an exception because the offset -3 not valid for São Paulo at a given date / time.