How can I convert a UTC string to a LocalDateTime with DST adjustment?

Chris :

I'm receiving a UTC timestamp string from an external API, and I need to store it as a LocalDateTime. In other words, if the timestamp is within a period when daylight saving is active, it should be adjusted to DST (usually by an hour).

I parse the incoming string to an OffsetDateTime, which I then convert to a ZonedDateTime, and then to an Instant. At this point, the DST time is correctly adjusted. But when I create a LocalDateTime from the Instant, it loses the adjustment.

  @Test
  public void testDates() {
    final DateTimeFormatter OFFSET_FORMAT = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSXX");
    final ZoneId zoneId = TimeZone.getDefault().toZoneId();

    final String summerTime = "2019-09-11T10:00:00.000+0000";
    final String winterTime = "2019-12-11T10:00:00.000+0000";

    OffsetDateTime odtSummer = OffsetDateTime.parse(summerTime, OFFSET_FORMAT);
    OffsetDateTime odtWinter = OffsetDateTime.parse(winterTime, OFFSET_FORMAT);

    ZonedDateTime zdtSummer = odtSummer.toLocalDateTime().atZone(zoneId);
    ZonedDateTime zdtWinter = odtWinter.toLocalDateTime().atZone(zoneId);

    Instant instSummer = zdtSummer.toInstant();
    Instant instWinter = zdtWinter.toInstant();

    System.out.println("instSummer = " + instSummer);  // instSummer = 2019-09-11T09:00:00Z
    System.out.println("instWinter = " + instWinter);  // instWinter = 2019-12-11T10:00:00Z

    LocalDateTime ldtSummer = LocalDateTime.ofInstant(instSummer, zoneId);
    LocalDateTime ldtWinter = LocalDateTime.ofInstant(instWinter, zoneId);

    System.out.println("ldtSummer = " + ldtSummer);  // ldtSummer = 2019-09-11T10:00
    System.out.println("ldtWinter = " + ldtWinter);  // ldtWinter = 2019-12-11T10:00
  }

How should I do this? I don't want to resort to something ugly like re-parsing Instant.toString().

Jon Skeet :

The problem is the way you're converting the inputs to ZonedDateTime values

ZonedDateTime zdtSummer = odtSummer.toLocalDateTime().atZone(zoneId);
ZonedDateTime zdtWinter = odtWinter.toLocalDateTime().atZone(zoneId);

Here you're saying "take the local date time version of the OffsetDateTime, and pretend that was actually a local value in the given time zone". So you end up with "10am local time in the time zone" rather than "10am UTC, converted to the local time zone".

You wrote that "At this point, the DST time is correctly adjusted" - but it's not. You started with a value of "2019-09-11T10:00:00.000+0000", but when you print the Instant it's printing "2019-09-11T09:00:00Z". 10am UTC and 9am UTC are not the same instant.

Instead, you should convert the OffsetDateTime to an Instant - as that's what you've really parsed - and then put that in the relevant time zone:

 ZonedDateTime zdtSummer = odtSummer.toInstant().atZone(zoneId);
 ZonedDateTime zdtWinter = odtWinter.toInstant().atZone(zoneId);

Or use the OffsetDateTime.atZoneSameInstant

 ZonedDateTime zdtSummer = odtSummer.atZoneSameInstant(zoneId);
 ZonedDateTime zdtWinter = odtSummer.atZoneSameInstant(zoneId);

Note that there's then no point in going from that back to an instant to get the LocalDateTime - just use toLocalDateTime. If you want all the relevant types, here's the appropriate code:

OffsetDateTime odtSummer = OffsetDateTime.parse(summerTime, OFFSET_FORMAT);
OffsetDateTime odtWinter = OffsetDateTime.parse(winterTime, OFFSET_FORMAT);

Instant instSummer = odtSummer.toInstant();
Instant instWinter = odtWinter.toInstant();

ZonedDateTime zdtSummer = instSummer.atZone(zoneId);
ZonedDateTime zdtWinter = instWinter.atZone(zoneId);

LocalDateTime ldtSummer = zdtSummer.toLocalDateTime();
LocalDateTime ldtWinter = zdtWinter.toLocalDateTime();

If you don't need the Instant, just:

OffsetDateTime odtSummer = OffsetDateTime.parse(summerTime, OFFSET_FORMAT);
OffsetDateTime odtWinter = OffsetDateTime.parse(winterTime, OFFSET_FORMAT);

ZonedDateTime zdtSummer = odtSummer.atZoneSameInstant(zoneId);
ZonedDateTime zdtWinter = odtWinter.atZoneSameInstant(zoneId);

LocalDateTime ldtSummer = zdtSummer.toLocalDateTime();
LocalDateTime ldtWinter = zdtWinter.toLocalDateTime();

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=323468&siteId=1