This code is trying to construct a OffsetDateTime from ZonedDateTime/LocalDatimeTime at Paris timezone.
ZoneId paris = ZoneId.of( "Europe/Paris" );
OffsetDateTime offsetDateTime = OffsetDateTime.of( 2018, 10, 28, 2, 0, 0, 0, ZoneOffset.of( "+01:00" ) );
ZonedDateTime zonedDateTime = offsetDateTime.toZonedDateTime();
System.out.println("ZonedDateTime: " + zonedDateTime);
zonedDateTime = zonedDateTime.withZoneSameInstant( paris );
System.out.println("zonedDateTime.withZoneSameInstant( ZoneId.of( \"Europe/Paris\" ) ): " + zonedDateTime);
System.out.println("zonedDateTime.toOffsetDateTime(): " + zonedDateTime.toOffsetDateTime());
System.out.println("================================================================================================");
LocalDateTime localDateTime2 = LocalDateTime.of( 2018, 10, 28, 2, 0, 0, 0 );
System.out.println("localDateTime2: " + localDateTime2);
ZonedDateTime zonedDateTime2 = ZonedDateTime.of( localDateTime2, paris );
System.out.println("ZonedDateTime.of( localDateTime2, ZoneId.of( \"Europe/Paris\" ) ): " + zonedDateTime2);
localDateTime2 = zonedDateTime2.toLocalDateTime();
System.out.println("zonedDateTime2.toLocalDateTime(): " + localDateTime2);
System.out.println("localDateTime2.atZone( paris ).toOffsetDateTime(): " + localDateTime2.atZone( paris ).toOffsetDateTime());
Run it will result:
ZonedDateTime: 2018-10-28T02:00+01:00
zonedDateTime.withZoneSameInstant( ZoneId.of( "Europe/Paris" ) ): 2018-10-28T02:00+01:00[Europe/Paris]
zonedDateTime.toOffsetDateTime(): 2018-10-28T02:00+01:00
================================================================================================
localDateTime2: 2018-10-28T02:00
ZonedDateTime.of( localDateTime2, ZoneId.of( "Europe/Paris" ) ): 2018-10-28T02:00+02:00[Europe/Paris]
zonedDateTime2.toLocalDateTime(): 2018-10-28T02:00
localDateTime2.atZone( paris ).toOffsetDateTime(): 2018-10-28T02:00+02:00
In sort, the result before ===================
is 2018-10-28T02:00+01:00
, while the result after ===================
is 2018-10-28T02:00+02:00
. 1 offset diff.
My question is, why does the second case have the offset to be +02:00
?
AFAIK, Paris has a complicated historical timezone, but it is/was stable at 2018, so the offset at that time should be +01:00
.
paris.getRules().getTransitions().forEach( System.out::println );
/*RESULT
Transition[Overlap at 1911-03-11T00:00+00:09:21 to Z]
Transition[Gap at 1916-06-14T23:00Z to +01:00]
Transition[Overlap at 1916-10-02T00:00+01:00 to Z]
Transition[Gap at 1917-03-24T23:00Z to +01:00]
Transition[Overlap at 1917-10-08T00:00+01:00 to Z]
Transition[Gap at 1918-03-09T23:00Z to +01:00]
Transition[Overlap at 1918-10-07T00:00+01:00 to Z]
Transition[Gap at 1919-03-01T23:00Z to +01:00]
Transition[Overlap at 1919-10-06T00:00+01:00 to Z]
Transition[Gap at 1920-02-14T23:00Z to +01:00]
Transition[Overlap at 1920-10-24T00:00+01:00 to Z]
Transition[Gap at 1921-03-14T23:00Z to +01:00]
Transition[Overlap at 1921-10-26T00:00+01:00 to Z]
Transition[Gap at 1922-03-25T23:00Z to +01:00]
Transition[Overlap at 1922-10-08T00:00+01:00 to Z]
Transition[Gap at 1923-05-26T23:00Z to +01:00]
Transition[Overlap at 1923-10-07T00:00+01:00 to Z]
Transition[Gap at 1924-03-29T23:00Z to +01:00]
Transition[Overlap at 1924-10-05T00:00+01:00 to Z]
Transition[Gap at 1925-04-04T23:00Z to +01:00]
Transition[Overlap at 1925-10-04T00:00+01:00 to Z]
Transition[Gap at 1926-04-17T23:00Z to +01:00]
Transition[Overlap at 1926-10-03T00:00+01:00 to Z]
Transition[Gap at 1927-04-09T23:00Z to +01:00]
Transition[Overlap at 1927-10-02T00:00+01:00 to Z]
Transition[Gap at 1928-04-14T23:00Z to +01:00]
Transition[Overlap at 1928-10-07T00:00+01:00 to Z]
Transition[Gap at 1929-04-20T23:00Z to +01:00]
Transition[Overlap at 1929-10-06T00:00+01:00 to Z]
Transition[Gap at 1930-04-12T23:00Z to +01:00]
Transition[Overlap at 1930-10-05T00:00+01:00 to Z]
Transition[Gap at 1931-04-18T23:00Z to +01:00]
Transition[Overlap at 1931-10-04T00:00+01:00 to Z]
Transition[Gap at 1932-04-02T23:00Z to +01:00]
Transition[Overlap at 1932-10-02T00:00+01:00 to Z]
Transition[Gap at 1933-03-25T23:00Z to +01:00]
Transition[Overlap at 1933-10-08T00:00+01:00 to Z]
Transition[Gap at 1934-04-07T23:00Z to +01:00]
Transition[Overlap at 1934-10-07T00:00+01:00 to Z]
Transition[Gap at 1935-03-30T23:00Z to +01:00]
Transition[Overlap at 1935-10-06T00:00+01:00 to Z]
Transition[Gap at 1936-04-18T23:00Z to +01:00]
Transition[Overlap at 1936-10-04T00:00+01:00 to Z]
Transition[Gap at 1937-04-03T23:00Z to +01:00]
Transition[Overlap at 1937-10-03T00:00+01:00 to Z]
Transition[Gap at 1938-03-26T23:00Z to +01:00]
Transition[Overlap at 1938-10-02T00:00+01:00 to Z]
Transition[Gap at 1939-04-15T23:00Z to +01:00]
Transition[Overlap at 1939-11-19T00:00+01:00 to Z]
Transition[Gap at 1940-02-25T02:00Z to +01:00]
Transition[Gap at 1940-06-14T23:00+01:00 to +02:00]
Transition[Overlap at 1942-11-02T03:00+02:00 to +01:00]
Transition[Gap at 1943-03-29T02:00+01:00 to +02:00]
Transition[Overlap at 1943-10-04T03:00+02:00 to +01:00]
Transition[Gap at 1944-04-03T02:00+01:00 to +02:00]
Transition[Overlap at 1944-10-08T01:00+02:00 to +01:00]
Transition[Gap at 1945-04-02T02:00+01:00 to +02:00]
Transition[Overlap at 1945-09-16T03:00+02:00 to +01:00]
Transition[Gap at 1976-03-28T01:00+01:00 to +02:00]
Transition[Overlap at 1976-09-26T01:00+02:00 to +01:00]
Transition[Gap at 1977-04-03T02:00+01:00 to +02:00]
Transition[Overlap at 1977-09-25T03:00+02:00 to +01:00]
Transition[Gap at 1978-04-02T02:00+01:00 to +02:00]
Transition[Overlap at 1978-10-01T03:00+02:00 to +01:00]
Transition[Gap at 1979-04-01T02:00+01:00 to +02:00]
Transition[Overlap at 1979-09-30T03:00+02:00 to +01:00]
Transition[Gap at 1980-04-06T02:00+01:00 to +02:00]
Transition[Overlap at 1980-09-28T03:00+02:00 to +01:00]
Transition[Gap at 1981-03-29T02:00+01:00 to +02:00]
Transition[Overlap at 1981-09-27T03:00+02:00 to +01:00]
Transition[Gap at 1982-03-28T02:00+01:00 to +02:00]
Transition[Overlap at 1982-09-26T03:00+02:00 to +01:00]
Transition[Gap at 1983-03-27T02:00+01:00 to +02:00]
Transition[Overlap at 1983-09-25T03:00+02:00 to +01:00]
Transition[Gap at 1984-03-25T02:00+01:00 to +02:00]
Transition[Overlap at 1984-09-30T03:00+02:00 to +01:00]
Transition[Gap at 1985-03-31T02:00+01:00 to +02:00]
Transition[Overlap at 1985-09-29T03:00+02:00 to +01:00]
Transition[Gap at 1986-03-30T02:00+01:00 to +02:00]
Transition[Overlap at 1986-09-28T03:00+02:00 to +01:00]
Transition[Gap at 1987-03-29T02:00+01:00 to +02:00]
Transition[Overlap at 1987-09-27T03:00+02:00 to +01:00]
Transition[Gap at 1988-03-27T02:00+01:00 to +02:00]
Transition[Overlap at 1988-09-25T03:00+02:00 to +01:00]
Transition[Gap at 1989-03-26T02:00+01:00 to +02:00]
Transition[Overlap at 1989-09-24T03:00+02:00 to +01:00]
Transition[Gap at 1990-03-25T02:00+01:00 to +02:00]
Transition[Overlap at 1990-09-30T03:00+02:00 to +01:00]
Transition[Gap at 1991-03-31T02:00+01:00 to +02:00]
Transition[Overlap at 1991-09-29T03:00+02:00 to +01:00]
Transition[Gap at 1992-03-29T02:00+01:00 to +02:00]
Transition[Overlap at 1992-09-27T03:00+02:00 to +01:00]
Transition[Gap at 1993-03-28T02:00+01:00 to +02:00]
Transition[Overlap at 1993-09-26T03:00+02:00 to +01:00]
Transition[Gap at 1994-03-27T02:00+01:00 to +02:00]
Transition[Overlap at 1994-09-25T03:00+02:00 to +01:00]
Transition[Gap at 1995-03-26T02:00+01:00 to +02:00]
Transition[Overlap at 1995-09-24T03:00+02:00 to +01:00]
Transition[Gap at 1996-03-31T02:00+01:00 to +02:00]
Transition[Overlap at 1996-10-27T03:00+02:00 to +01:00]
Transition[Gap at 1997-03-30T02:00+01:00 to +02:00]
Transition[Overlap at 1997-10-26T03:00+02:00 to +01:00]*/
Where does +02:00
come from?
On 2018-10-28, Europe/Paris
underwent an overlap transition. That is, the clocks "went backwards" as a result of daylight saving time ended.
So there were two instances of "2am" in Paris. The clocks reach the first instance of "2am", then as they are about to reach 3am, the timezone offset changes, and instead of reaching 3am, the clocks go back to 2am, hence the second instance.
This page probably explains it better than I did.
When you use withZoneSameInstant
, what the result should be is totally unambiguous - OffsetDateTime
represents some instant in time, and there is a unique ZonedDateTime
that represents that instant.
However, when you have only a LocalDateTime
and a ZoneId
, it is unclear what the resulting ZonedDateTime
should be. Your LocalDateTime
just says 2am, but Europe/Paris
had two instances of 2am that day, so this is ambiguous. The documentation says that ZonedDateTime
will always choose the one that is earlier in time.
In most cases, there is only one valid offset for a local date-time. 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".
If you prefer the second instance of 2am instead, you can use ofLocal
, where you can specify your preferred offset.
LocalDateTime localDateTime = LocalDateTime.of(2018, 10, 28, 2, 0, 0, 0);
// during an overlap transition, the second element of this list will have the post-transition offset
var validOffsets = paris.getRules().getValidOffsets(localDateTime);
// getLast() is available since Java 21. Use get(validOffsets.size() - 1) on earlier versions
var preferredOffset = validOffsets.isEmpty() ? null : validOffsets.getLast();
ZonedDateTime.ofLocal(localDateTime, paris, preferredOffset);
Alternatively, you can use withLaterOffsetAtOverlap
to get the second instance of 2am from the first instance.
var zonedDateTime = ZonedDateTime.of(localDateTime, paris).withLaterOffsetAtOverlap()
Also note what happens in the case of a gap transition, where the clocks move forward:
In the case of a gap, where 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".