7
ZonedDateTime zdt = ZonedDateTime.of(2015, 10, 18, 0, 30, 0, 0,
    ZoneId.of("America/Sao_Paulo")); 
System.out.println(zdt); // 2015-10-18T01:30-02:00[America/Sao_Paulo]

You can see the hour is 1 while we set the hour as 0, and timezone is UTC-02:00 while daylight saving timezone should be UTC-03:00.

But here is a different example:

ZonedDateTime zdt = ZonedDateTime.of(2015, 10, 18, 0, 30, 0, 0,
    ZoneId.of("America/Los_Angeles"));
System.out.println(zdt); //2015-10-18T00:30-07:00[America/Los_Angeles]

You can see the daylight saving timezone is UTC-07:00 and the hour is 0 as we set.

Why are they different?

Hao Ma
  • 711
  • 7
  • 8

2 Answers2

8

This happens because the time you picked falls in the gap between midnight and 01:00 on the night when Brazil switches to summer time. That time is actually impossible and so you get the behavior described in the documentation:

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 into the offset typically corresponding to "summer".

You can observe the same behavior in Los_Angeles zone by picking a time between 02:00 and 03:00 on the corresponding night in March:

zdt = ZonedDateTime.of(2015, 3, 8, 2, 30, 0, 0,
        ZoneId.of("America/Los_Angeles"));
System.out.println(zdt); 
Misha
  • 27,433
  • 6
  • 62
  • 78
4

As already explained in @Misha's answer, it happens due to Daylight Saving Time rules.

In São Paulo, DST starts at the midnight of 2015-10-18: the clocks move forward 1 hour, so it "jumps" from 23:59:59 to 01:00:00. There's a gap between 00:00:00 and 00:59:59, so the time 00:30 is adjusted accordingly.

You can check if the date and time are valid for the timezone using the ZoneRules and ZoneOffsetTransition classes:

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 the valid offsets for the specified date/time. If the list is empty, it means the date/time doesn't "exist" in that timezone (usually because of DST the clocks jump forward).

When the date/time exists in a timezone, an 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

For Los_Angeles timezone, 1 valid offset is returned: -07:00.

PS: Offset changes usually occur due to DST, but that's not always the case. DST and offsets are defined by governments and laws, and they can change at anytime. So, a gap in the valid offset can also mean that such change occured (some politician decided to change the standard offset of the country, so the gap might not necessarily be related to DST).

You can also check when the change occurs, and what's the offset before and after it:

ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();

// get the previous transition (the last one that occurred before 2015-10-18 00:30 in Sao_Paulo timezone 
ZoneOffsetTransition t = rules.previousTransition(dt.atZone(sp).toInstant());
System.out.println(t);

The output is:

Transition[Gap at 2015-10-18T00:00-03:00 to -02:00]

It means that there's a gap (clocks moving forward) at 2015-10-18T00:00, and the offset will change from -03:00 to -02:00 (so, clock moves 1 hour forward).

You can also get all these info separately:

System.out.println(t.getDateTimeBefore() + " -> " + t.getDateTimeAfter());
System.out.println(t.getOffsetBefore() + " -> " + t.getOffsetAfter());

The output is:

2015-10-18T00:00 -> 2015-10-18T01:00
-03:00 -> -02:00

It shows that, at 00:00 the clock moves directly to 01:00 (so 00:30 can't exist). In the second line, the offsets before and after the change.


If you check the transitions in Los_Angeles timezone, you'll see that its DST starts and ends at 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));

The output is:

Transition[Gap at 2015-03-08T02:00-08:00 to -07:00]
Transition[Overlap at 2015-11-01T02:00-07:00 to -08:00]

So, in Los_Angeles timezone, DST starts at 2015-03-08 and ends at 2015-11-01. That's why at 2015-10-18, all hours are valid (there's no adjustment as it happens in Sao_Paulo timezone).


Some timezones have transition rules (like "DST starts at the third Sunday of October") instead of just transitions (like "DST starts at this specific date and time"), and you can also use them, if available:

ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();

// hardcoded: Sao_Paulo timezone has 2 transition rules, the second one is relative to October
// but you should always check if the list is not empty
ZoneOffsetTransitionRule tr = rules.getTransitionRules().get(1);
// get the transition for year 2015
ZoneOffsetTransition t = tr.createTransition(2015);
// use t the same way as above (the output will be the same)

Another way to check if a date and time is valid for some timezone is to use the ZonedDateTime.ofStrict method, that throws an exception if the date and time is invalid for a timezone:

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 OK, because an offset of -7 is valid for Los Angeles, for the given date/time. The second case throws an exception because an offset of -3 is invalid for São Paulo, at the given date/time.