silent-box
silent-box

Reputation: 1666

Java 8 Parse ISO-8601 date ignoring presence (or absence) of timezone

My application should be able to parse date ignoring timezone (I always know for sure that it is UTC). The problem is that the date might come in both following forms -

2017-09-11T12:44:07.793Z

0001-01-01T00:00:00

I can parse the first one using LocalDateTime, and the second one using Instant class. Is there a way to do that using a single mechanism?

P.S. I'm trying to avoid hardcoding Z at the end of the input string

Upvotes: 8

Views: 3677

Answers (2)

M. Prokhorov
M. Prokhorov

Reputation: 3993

You can "parseBest", like this:

DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[Z]");

Temporal parsed = parser.parseBest(inputString, Instant::from, LocalDateTime::from);

Then you should check what did get parsed, and act accordingly. The parseBest method will work with any type of TemporalQuery, including most of from methods available on java.time classes. So you can lengthen that list with LocalDate.from, for example.

You can also use that method and lambdas to coerse parse results to the type you want without having instanceof checks that are external for result resolution (although not without one cast):

Instant parsed = (Instant) parser.parseBest(inputString,
                    Instant::from,
                    interResult -> LocalDateTime.from(interResult).atZone(ZoneOffset.UTC).toInstant())

Notice that second option uses lambda that converts LocalDateTime to ZonedDateTime and then to Instant, so the parse results are always coersed to Instant.

Upvotes: 5

user7605325
user7605325

Reputation:

If the Z offset is optional, you can use a java.time.format.DateTimeFormatterBuilder with an optional section:

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // date/time
    .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
    // optional offset
    .optionalStart().appendOffsetId()
    // create formatter
    .toFormatter();

Then you can use the parseBest method, with a list of TemporalQuery's that tries to create the correspondent object. Then you check the return type and act accordingly:

Instant instant = null;
// tries to create Instant, and if it fails, try a LocalDateTime
TemporalAccessor parsed = fmt.parseBest("2017-09-11T12:44:07.793Z", Instant::from, LocalDateTime::from);
if (parsed instanceof Instant) {
    instant = (Instant) parsed;
} else if (parsed instanceof LocalDateTime) {
    // convert LocalDateTime to UTC instant
    instant = ((LocalDateTime) parsed).atOffset(ZoneOffset.UTC).toInstant();
}
System.out.println(instant); // 2017-09-11T12:44:07.793Z

Running with the second input (0001-01-01T00:00:00) produces the Instant equivalent to 0001-01-01T00:00:00Z.

In the example above, I used just Instant::from and LocalDateTime::from, so the formatter tries to first create an Instant. If it's not possible, then it tries to create a LocalDateTime. You can add as many types you want to that list (for example, I could add ZonedDateTime::from, and if a ZonedDateTime is created, I could just convert to Instant using toInstant() method).


As you know for sure that the input is always in UTC, you can also set it directly in the formatter:

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // date/time
    .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
    // optional offset
    .optionalStart().appendOffsetId()
    // create formatter with UTC
    .toFormatter().withZone(ZoneOffset.UTC);

So you can parse it directly to Instant:

System.out.println(Instant.from(fmt.parse("2017-09-11T12:44:07.793Z"))); // 2017-09-11T12:44:07.793Z
System.out.println(Instant.from(fmt.parse("0001-01-01T00:00:00"))); // 0001-01-01T00:00:00Z

Upvotes: 15

Related Questions