2

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().

Chris
  • 1,038
  • 10
  • 18
  • 1
    Could you adjust your sample to use a specific time zone, so we can all reproduce your results, please? – Jon Skeet Oct 25 '19 at 07:45
  • 1
    [@BasilBourqe](https://stackoverflow.com/users/642706/basil-bourque)'s stuff doesn't answer it, starting with [this one](https://stackoverflow.com/questions/32437550/whats-the-difference-between-instant-and-localdatetime)? – Curiosa Globunznik Oct 25 '19 at 08:00
  • Since you’ve got a specific point in time (an instant), general recommendations say that you should not try to represent it in a `LocalDateTime` since this class *cannot* represent a specific point in time. This includes not using `odtSummer.toLocalDateTime()` and `odtWinter.toLocalDateTime()`. Instead keep the time in your time zone in a `ZonedDateTime`. – Ole V.V. Oct 25 '19 at 09:39

2 Answers2

5

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();
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    Why go though `Instant`, when [`atZoneSameInstant()`](https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html#atZoneSameInstant-java.time.ZoneId-) exists? – Andreas Oct 25 '19 at 08:05
  • @Andreas: Will add that as an option as well. It's not clear how many of the types are actually required here. – Jon Skeet Oct 25 '19 at 08:05
  • I read question as only `LocalDateTime` required, and OP just printing the intermediate values to help figure out where things went wrong. – Andreas Oct 25 '19 at 08:06
  • @Andreas: It's hard to know for sure, so I've given a few options :) – Jon Skeet Oct 25 '19 at 08:07
  • Thanks Jon Skeet and @Andreas. Both very clear and helpful solutions. – Chris Oct 25 '19 at 09:50
3

When you do odtSummer.toLocalDateTime(), you're discarding the fact that the input date/time is UTC, so you've lost the information right there.

Instead, convert the OffsetDateTime to a ZonedDateTime the desired time zone, by calling atZoneSameInstant(zoneId).

Then get the LocalDateTime from that, by calling toLocalDateTime().

FYI: Use ZoneId.systemDefault() to get default time zone, not TimeZone.getDefault().toZoneId().

final DateTimeFormatter OFFSET_FORMAT = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSXX");
final ZoneId zoneId = ZoneId.of("America/New_York"); // or ZoneId.systemDefault()

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); // 2019-09-11T10:00Z
OffsetDateTime odtWinter = OffsetDateTime.parse(winterTime, OFFSET_FORMAT); // 2019-12-11T10:00Z

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

System.out.println("zdtSummer = " + zdtSummer);  // 2019-09-11T06:00-04:00[America/New_York]
System.out.println("zdtWinter = " + zdtWinter);  // 2019-12-11T05:00-05:00[America/New_York]

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

System.out.println("ldtSummer = " + ldtSummer);  // 2019-09-11T06:00
System.out.println("ldtWinter = " + ldtWinter);  // 2019-12-11T05:00
Andreas
  • 154,647
  • 11
  • 152
  • 247