Search code examples
javatimezonejava-timelocaltimejava.time.instant

How to find all Instants which correspond to the given LocalDateTime and ZoneId?


When converting Instant to LocalDateTime it may happen that several different Instants 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 Instants which are in the given time zone mapped to the given LocalDateTime:

List<Instant> localToInstants(LocalDateTime dt, ZoneId zone)

Solution

  • Yes ✔️

    Edit

    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 Instants 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.

    Original answer

    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 ZonedDateTimes 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.

    Documetation link

    ZoneRules.getValidOffsets()