7

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

silent-box
  • 1,392
  • 1
  • 15
  • 32
  • What should be the result? Always a `LocalDateTime`? –  Sep 25 '17 at 17:19
  • The goal is to get an Instant – silent-box Sep 25 '17 at 17:21
  • @silent-box, you can't get an `Instant` from `0001-01-01T00:00:00` without hardcoding time zone inside the parser. – M. Prokhorov Sep 25 '17 at 17:27
  • 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 additions – silent-box Sep 25 '17 at 17:30
  • 1
    @silent-box 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. – Basil Bourque Sep 26 '17 at 00:10

2 Answers2

11

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
  • 1
    Thank you for such a detailed answer! I really like the builder, will use. – silent-box Sep 25 '17 at 17:33
  • 2
    @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. – M. Prokhorov Sep 25 '17 at 17:47
4

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.

M. Prokhorov
  • 3,517
  • 20
  • 34
  • *trying to avoid hardcoding Z at the end of the string* in the question – Naman Sep 25 '17 at 17:17
  • @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? – M. Prokhorov Sep 25 '17 at 17:25
  • Although I suppose there are optional pattern parts. But `Z` will still be in the string, so guess it's not possible? – M. Prokhorov Sep 25 '17 at 17:26
  • As I mentioned above, I meant don't want hardcode `Z` into the input string. It's ok to have `Z` in the pattern :) – silent-box Sep 25 '17 at 17:32
  • 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);` – silent-box Sep 25 '17 at 17:32
  • 1
    @silent-box, cool. Hugo's answer looks more fleshed out, so if what he suggested worked, please mark his answer as accepted. – M. Prokhorov Sep 25 '17 at 17:33
  • @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);` – silent-box Sep 25 '17 at 17:35
  • 1
    @Hugo, ah, so it does always need optional parts if one wants to `parseBest`. Noted. I'll update. – M. Prokhorov Sep 25 '17 at 17:36
  • 1
    @silent-box 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). –  Sep 25 '17 at 17:36
  • 1
    @silent-box 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. –  Sep 25 '17 at 17:40
  • 2
    @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. – M. Prokhorov Sep 25 '17 at 17:45
  • I get it, now it's all clear. Thank you once again guys! – silent-box Sep 25 '17 at 17:48