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

2

8

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

Eartha answered 25/9, 2017 at 17:10 Comment(6)
What should be the result? Always a LocalDateTime?Ganger
The goal is to get an InstantEartha
@silent-box, you can't get an Instant from 0001-01-01T00:00:00 without hardcoding time zone inside the parser.Justiciable
no-no, I meant I don't want to concat additional Z in the end of the input string. It's ok to hardcode UTC in the code. Your answer is correct and looks like it work for me with small additionsEartha
@Eartha Exactly how is "hard-coding UTC in the code" really different from "concat additional Z"? They both have the very same effect. Except that concatenating the Z makes your intentions quite clear in very little code: Correcting faulty input data.Thrombo
@BasilBourque Concatenating a "Z" to every string that's parsed can easily lead to faulty code and is considered bad practice, not to mention that it would be unnecessary and wasteful for a large number of strings. It is much better to establish intentions through the object being used and to well-document clarifying explanations of the purpose of the code and its behavior. Consider the selected answer and its code snippet, which does this and includes short, but well-clarifying comments easing understanding even for those mostly unfamiliar with the API.Kirven
G
15

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
Ganger answered 25/9, 2017 at 17:27 Comment(2)
Thank you for such a detailed answer! I really like the builder, will use.Eartha
@silent-box, note that it's not imperative to use existing method references when calling parseBest: you can write your own methods that will produce desired types of Temporal objects. I have an example in my answer.Justiciable
J
5

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.

Justiciable answered 25/9, 2017 at 17:16 Comment(12)
trying to avoid hardcoding Z at the end of the string in the questionHip
@nullpointer, I don't even know what that'd look like. Is there also a goal of not hardcoding yyyy in the pattern or something equally strange?Justiciable
Although I suppose there are optional pattern parts. But Z will still be in the string, so guess it's not possible?Justiciable
As I mentioned above, I meant don't want hardcode Z into the input string. It's ok to have Z in the pattern :)Eartha
Here's what works for me in both cases: DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSSX]").parseBest("2017-09-11T12:44:07.793Z", Instant::from, LocalDateTime::from);Eartha
@silent-box, cool. Hugo's answer looks more fleshed out, so if what he suggested worked, please mark his answer as accepted.Justiciable
@M.Prokhorov, just did that. Thanks again. @Hugo, it works indeed: DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSSX]").parseBest("0001-01-01T00:00:00", Instant::from, LocalDateTime::from);Eartha
@Hugo, ah, so it does always need optional parts if one wants to parseBest. Noted. I'll update.Justiciable
@Eartha I was talking about yyyy-MM-dd'T'HH:mm:ssZ pattern, it doesn't work for 0001-01-01T00:00:00. Also, parseBest returns a TemporalAccessor (so the code in the answer requires a cast to Temporal as well).Ganger
@Eartha Another detail is that when using [.SSSX], both fraction of seconds and offset will be optional (so either you have both, or none). Using DateTimeFormatterBuilder (as in my answer), the fraction of seconds can be optional regardless of the offset (so just one of them can be in the input, or both, or none). A little detail, but you must check which one fits best to your inputs.Ganger
@silent-box, Hugo is correct, if you want fracion seconds to be optional, and you want your pattern as string, then it's better to have [.SSS][X] at the very least, so there's two independent optional sections in pattern.Justiciable
I get it, now it's all clear. Thank you once again guys!Eartha

© 2022 - 2024 — McMap. All rights reserved.