Java Time API converting ISO 8601 to ZonedDateTime

4.9k views Asked by At

I am having a hard time converting an ISO 8601 formatted String to a java.time.LocalDateTime which is in UTC.

More specific, I am trying to write an XMLAdapter for which you can enter the various ISO 8601 dataformats as a String (i.e. 2002-09-24, 2011-03-22T13:30, 2015-05-24T12:25:15Z, 2015-07-28T11:11:15.321+05:30) and which outputs a LocalDateTime in UTC and visa versa.

The system stores all it's Date and Time information internal in UTC times. When a user requests a Date or Time it is represented to the user based on their own ZoneId.

4

There are 4 answers

1
softarn On BEST ANSWER

Edit: Basils answer below should be marked correct. https://stackoverflow.com/a/43083698/348956

As the name suggests LocalDateTime holds both Date and Time. The first example of a date string you have in your question for example only holds information about the date, therefore you cannot parse this directly into a LocalDateTime. What you could do there is to first parse it into a LocalDate and by setting the time on that object get a LocalDateTime.

LocalDateTime localDateTime = LocalDate.parse("2002-09-24").atStartOfDay();

All Date and Time objects have a parse method like LocalDate which can take a certain string format. These formats are different ISO standard formats specified in DateTimeFormatter

For formatting custom datetime strings into Temporal objects use DateTimeFormatter and specify a custom pattern.

0
Martijn Burger On
public class LocalDateTimeXmlAdapter extends XmlAdapter<String, LocalDateTime> {

    private static final Pattern ZONE_PATTERN = Pattern.compile("T.*(\\+|\\-|Z)");

    @Override
    public LocalDateTime unmarshal(String isoDateTime) throws Exception {
        LocalDateTime utcDateTime;
        if (ZONE_PATTERN.matcher(isoDateTime).matches()) {
            OffsetDateTime offsetDateTime = OffsetDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME);
            ZoneOffset offset = offsetDateTime.getOffset();
            utcDateTime = offsetDateTime.toLocalDateTime().plusSeconds(offset.getTotalSeconds());
        } else {
            LocalDateTime localDateTime = LocalDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME);
            ZoneId zoneId = ZoneId.systemDefault(); // TODO: Get ZoneId from userProfile
            ZoneOffset offset = ZonedDateTime.now(zoneId).getOffset();
            utcDateTime = localDateTime.minusSeconds(offset.getTotalSeconds());
        }
        return utcDateTime;
    }

    @Override
    public String marshal(LocalDateTime utcDateTime) throws Exception {
        ZoneId zoneId = ZoneId.systemDefault(); // TODO: Get ZoneId from userProfile
        ZoneOffset offset = ZonedDateTime.now(zoneId).getOffset();
        return utcDateTime.plusSeconds(offset.getTotalSeconds()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }

}
0
John Deverall On

There are various formatters defined on DateTimeFormatter which do the job.

For example:

TemporalAccessor accessor= DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse("2020-07-14T21:56:00Z");
ZonedDateTime from = ZonedDateTime.from(accessor);

OR with a different offset format still in iso 8601 spec:

TemporalAccessor accessor= DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse("2020-07-14T21:56:00+00:00");
ZonedDateTime from = ZonedDateTime.from(accessor);

Note: This only works for the 'extended' iso 8601 formats. I didn't find support for the simplified one (without -'s for example 20200714T215600Z) but I don't think that's a big deal. I just wont support the simplified format in my API. If anyone does find a way to support the iso 8601 simplified date format (not just the extended) that is not a lot of work and is happy to leave a solution that would be great :)

0
Basil Bourque On

tl;dr

Instant.parse( "2015-05-24T12:25:15Z" )
       .atZone( 
           ZoneId.of( "America/Montreal" ) 
       )
       .toString()

2015-05-24T08:25:15-04:00[America/Montreal]

ZonedDateTime vs LocalDateTime

Your Question contradicts itself, with the title asking for a ZonedDateTime and the body asking for a LocalDateTime. Those are two very different beasts. One (ZonedDateTime) is a specific moment on the timeline, the other (LocalDateTime) is only a vague idea about possible moments but is not a specific moment.

For example, the LocalDateTime of Christmas starting this year is 2017-12-25T00:00:00 but that has no meaning until you apply a time zone as Santa delivers first to the islands of Kiribati at their midnight (the first midnight on Earth), then on to New Zealand at their later midnight, then on to Australia at their later midnight, and so on moving westward toward successive midnights.

Also, you Question seems confused about exactly what is the input and what is the output.

a java.time.LocalDateTime which is in UTC.

This is a contradiction in terms. The LocalDateTime class lacks any concept of time zone or offset-from-UTC. So this class cannot be used to represent a moment such as a UTC value. For a moment in UTC, use either Instant or OffsetDateTime classes.

Instant

For an input like 2015-05-24T12:25:15Z, that represents a moment on the timeline in UTC (Z is short for Zulu and means UTC). To represent that, use the Instant class. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

Instant instant = Instant.parse( "2015-05-24T12:25:15Z" );

Generally you should be working in UTC for most business logic, data storage, data exchange, and database. So for that Instant is all you need.

ZonedDateTime

If you need to adjust into a time zone, such as for presentation to a user, apply a ZoneId to get a ZonedDateTime.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = instant.atZone( z );

LocalDate

If you have a date-only value, parse as a LocalDate object.

LocalDate localDate = LocalDate.parse( "2002-09-24" );

To get the first moment of the day for that date, specify a time zone. For any given moment, the date varies around the world by zone.

Also, do not assume the day starts at the time of 00:00:00. Anomalies such as Daylight Saving Time (DST) may cause the first moment to be something like 01:00:00. Let java.time determine the first moment’s time-of-day.

ZonedDateTime zdt = localDate.atStartOfDay( z );

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.