When converting Instant
to LocalDateTime
it may happen that several different Instant
s are converted into the same LocalDateTime
. Eg. in time zones with day light saving time.
My question is whether it's possible to write a general function which works for any ZoneId
and LocalDateTime
and returns all Instant
s which are in the given time zone mapped to the given LocalDateTime
:
List<Instant> localToInstants(LocalDateTime dt, ZoneId zone)
Yes ✔️
M. Prokhorov’s idea of using ZoneRules.getValidOffset()
gives more elegant code than I had in my original answer. Here’s a version that returns a list of Instant
s as requested:
public static List<Instant> localToInstants(LocalDateTime dt, ZoneId zone) {
return zone.getRules()
.getValidOffsets(dt)
.stream()
.map(dt::atOffset)
.map(OffsetDateTime::toInstant)
.collect(Collectors.toList());
}
Let’s try it out on a couple of non-trivial examples. Malta will transition to summer time on March 28. At 2 AM the clocks are turned forward to 3. So the time 2:30 does not exist on this day. Which means that we expect an empty list from the following call:
LocalDateTime dt = LocalDateTime.of(2021, Month.MARCH, 28, 2, 30);
System.out.println(localToInstants(dt, ZoneId.of("Europe/Malta")));
Output is indeed:
[]
This is Java’s way of printing an empty list. How it works: Since the time does not exist, ZoneRules.getValidOffsets()
returns an empty list. The rest is boring: There are no offsets to convert to OffsetDateTime
and further to Instant
, so the empty list is returned.
My other example is from Eater Island. According to my Java 9 summer time will end on May 8. At 22 the clocks will be turned back to 21. So the time 21:30 occurs twice. Let’s see:
LocalDateTime dt = LocalDateTime.of(2021, Month.MAY, 8, 21, 30);
System.out.println(localToInstants(dt, ZoneId.of("Pacific/Easter")));
[2021-05-09T02:30:00Z, 2021-05-09T03:30:00Z]
Since the UTC offset changes from -05:00 to -06:00, the instants are correct. In this case getValidOffsets()
returned two valid offsets, -05:00 and -06:00, so the code produced two OffsetDateTime
objects which were in turn converted to two different Instant
objects.
I am letting this stand in case anyone is interested in getting the appropriate ZonedDateTime
objects.
public static List<Instant> localToInstants(LocalDateTime dt, ZoneId zone) {
return Stream.of(dt.atZone(zone).withEarlierOffsetAtOverlap(),
dt.atZone(zone).withLaterOffsetAtOverlap())
.filter(zdt -> zdt.toLocalDateTime().equals(dt))
.map(ZonedDateTime::toInstant)
.distinct()
.collect(Collectors.toList());
}
Let’s try it out on a couple of non-trivial examples. Malta will transition to summer time on March 28. At 2 AM the clocks are turned forward to 3. So the time 2:30 does not exist on this day. Which means that we expect an empty list from the following call:
LocalDateTime dt = LocalDateTime.of(2021, Month.MARCH, 28, 2, 30);
System.out.println(localToInstants(dt, ZoneId.of("Europe/Malta")));
Output is indeed:
[]
This is Java’s way of printing an empty list. How it works: Since the time does not exist, dt.atZone(zone)
gives us 3:30 instead of 2:30. Java adds the length of the gap, one hour as usual for a summer time transition. The calls to withEarlierOffsetAtOverlap()
and withLaterOffsetAtOverlap()
make no difference in this case (I will return to them), so we just get the same ZonedDateTime
twice. The call to filter()
is what makes the difference: we convert ZonedDateTime
back to LocalDateTime
and discover that we didn’t get the same time, therefore we discard the results. The rest is boring: there are no ZonedDateTime
s to convert to Instant
, so we end up with an empty list.
My other example is from Eater Island. According to my Java 9 summer time will end on May 8. At 22 the clocks will be turned back to 21. So the time 21:30 occurs twice. Let’s see:
LocalDateTime dt = LocalDateTime.of(2021, Month.MAY, 8, 21, 30);
System.out.println(localToInstants(dt, ZoneId.of("Pacific/Easter")));
[2021-05-09T02:30:00Z, 2021-05-09T03:30:00Z]
Since the UTC offset changes from -05:00 to -06:00, the instants are correct. How it works: here’s where the calls to withEarlierOffsetAtOverlap()
and withLaterOffsetAtOverlap()
come into play. dt.atZone(zone)
gives us one of the options (2021-05-08T21:30-05:00[Pacific/Easter]
, so the earlier one, but no sane person remembers that by heart). Since Java knows that there’s an overlap at this time, withEarlierOffsetAtOverlap()
and withLaterOffsetAtOverlap()
give us the two different possibilities. Since both convert back to the correct LocalDateTime
, nothing is filtered out. And since both are different instants, distinct()
does not filter anything out either. We end up with two Instant
objects.