Java ZonedDateTime MST to UTC conversion uses daylight savings

325 Views Asked by At

I need to convert the timestamp to UTC. Timestamp has the MST timezone, which is specified in the timestamp

Examples of observed conversions:

  • 2023-03-20 08:53:19 MST -> 2023-03-20T14:53:19 (-6)
  • 2023-01-20 08:53:19 MST -> 2023-01-20T15:53:19 (-7)
static DateTimeFormatter EVENT_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzz");

static LocalDateTime timestampConverter(String timestamp) {
    return ZonedDateTime.parse(timestamp, EVENT_TIMESTAMP_FORMATTER)
            .withZoneSameInstant(ZoneOffset.UTC)
            .toLocalDateTime();
}

MDT offset from UTC is -6:00 hours

MST offset from UTC is -7:00 hours

Problem: Java conversion treats MST as MDT which observe daylight saving

For example JS library doesn't have this problem:

console.log(new Date("2023-03-20 08:53:19 MST").toUTCString()); // Mon, 20 Mar 2023 15:53:19 GMT

Question: Why logic is different from the standard behavior ?

3

There are 3 best solutions below

2
Stephen Olujare On

First and foremost, let's define the error in the provided snippet.

I realize that the snippet above treats the MST timezone as MDT and does not take daylight saving into account. I hope you understand that.

Now, let's fix the issue, you can use the ZoneId class to specify the time zone with daylight saving. Use this updated version of the timestampConverter method and that should work for both MST and MDT:

static LocalDateTime timestampConverter(String timestamp) {
ZonedDateTime zonedDateTime = ZonedDateTime.parse(timestamp,EVENT_TIMESTAMP_FORMATTER);
    if(zonedDateTime.getZone().getRules().isDaylightSavings(zonedDateTime.toInstant())){
    zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toLocalDateTime();
    }else{
        return zonedDateTime.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
    }
}

This should fix the challenge you are currently going through! And if you need more clarification... Don't hesitate. Happy Coding!

7
VGR On

This is happening because ZonedDateTime.parse uses ZonedDateTime.from, whose documentation states:

The conversion will first obtain a ZoneId from the temporal object, falling back to a ZoneOffset if necessary.

So MST is converted to a ZoneId, which always has DST rules in effect.

I had hoped that the version of DateTimeFormatterBuilder.appendZoneText which accepts preferred zone overrides might help, but it doesn’t. The preferred zones are only consulted for zone IDs which are considered ambiguous, and apparently MST is not considered ambiguous.

I couldn’t find any way for a single DateTimeFormatter to do what you want, but you can at least provide zone overrides when looking up a zone explicitly, using the two-argument ZoneId.of method:

static DateTimeFormatter EVENT_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

private static final Map<String, String> nonDSTOverrides = Map.of(
        "PST", "UTC-08:00",
        "MST", "UTC-07:00",
        "CST", "UTC-06:00",
        "EST", "UTC-05:00");

static LocalDateTime timestampConverter(String timestamp) {
    int lastSpace = timestamp.lastIndexOf(' ');
    if (lastSpace < 0) {
        throw new DateTimeParseException(
            "Text must contain a space before the timezone.", timestamp, 0);
    }

    String localPart = timestamp.substring(0, lastSpace);
    String zonePart = timestamp.substring(lastSpace + 1);

    return ZonedDateTime.of(
            LocalDateTime.parse(localPart, EVENT_TIMESTAMP_FORMATTER),
            ZoneId.of(zonePart, nonDSTOverrides))
        .withZoneSameInstant(ZoneOffset.UTC)
        .toLocalDateTime();
}
3
Dima Garbar On

I used this code to achieve the -07:00 conversion all the time.

    LocalDateTime timestampConverter(String timestamp) {
        return ZonedDateTime.parse(timestamp, EVENT_TIMESTAMP_FORMATTER)
                // Java conversion treats MST as MDT which observe daylight saving
                .withZoneSameLocal(ZoneId.of(ZONE_OFFSET))
                .withZoneSameInstant(ZoneOffset.UTC)
                .toLocalDateTime();
    }