Text '2021-06-22T18:27:03.5577Z' could not be parsed at index 20

Viewed 1135

I have several code snippets. Some of them works and some not but I don't understand why.

DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'");  //(7 positions after last dor)

TIME_FORMATTER.parse("2021-06-22T18:27:03.5577Z")//Broken 4 
TIME_FORMATTER.parse("2021-06-22T18:27:03.55770Z")//Broken 5
TIME_FORMATTER.parse("2021-06-22T18:27:03.557700Z")//Working 6 
TIME_FORMATTER.parse("2021-06-22T18:27:03.5577000Z")//Working 7 
TIME_FORMATTER.parse("2021-06-22T18:27:03.55770000Z")//Broken 8

See this code work, or not work, when running live at IdeOne.com.

Why it works for both: 6 and 7 digits after the decimal separator, but not 4, 5, or 8 digits?

How to create a formatter which would work for 4, 5, 6, 7, or 8 digits?

4 Answers

You can use optional formats in the pattern to parse the expected formats. For your specific case, the following works.

DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.[SSSSSSSS][SSSSSSS][SSSSS][SSSS]'Z'");

It is not very clear in the documentation, but the order of the optional sections matter.

DateTimeFormatter documentation

Also, this form of parsing has performance impacts and the usage of DateTimeFormatterBuilder is advised.

tl;dr

You asked:

How to create Format which will work for 4,5,6,7 numbers after point ?

Use the predefined formatter, DateTimeFormatter.ISO_INSTANT.

DateTimeFormatter.ISO_INSTANT.parse("2021-06-22T18:27:03.5577Z")

Never ignore Z

Never put quote-marks around Z in your formatting pattern. That letter is carries crucial information, and is not mere decoration. That letter indicates an offset from UTC of zero hours-minutes-seconds. Your single-quote-marks around Z indicate that letter should be expected and then ignored.

When ignoring the offset, you are left with a date and time only. A date and time are not enough to represent a moment. We cannot know if your input meant 6:30 PM in Tokyo, 6:30 PM in Toulouse, or 6:30 PM in Toledo — all very different moments several hours apart.

For a point on the timeline we require a third piece, the context of an offset or a time zone.

java.time.Instant.parse

Your input text complies with the ISO 8601 standard used by default in java.time. So no need to specify a formatting pattern.

Simply parse the input as an Instant object. The Instant represents a moment as seen in UTC, with an offset of zero.

The Instant.parse method uses the predefined formatter in the constant DateTimeFormatter.ISO_INSTANT.

Instant instant4 = Instant.parse("2021-06-22T18:27:03.5577Z") ;
Instant instant5 = Instant.parse("2021-06-22T18:27:03.55770Z") ;
Instant instant6 = Instant.parse("2021-06-22T18:27:03.557700Z") ;
Instant instant7 = Instant.parse("2021-06-22T18:27:03.5577000Z") ;
Instant instant8 = Instant.parse("2021-06-22T18:27:03.55770000Z") ;

See this code run live at IdeOne.com.

2021-06-22T18:27:03.557700Z
2021-06-22T18:27:03.557700Z
2021-06-22T18:27:03.557700Z
2021-06-22T18:27:03.557700Z

If curious as to how ISO_INSTANT is written in OpenJDK, see the source code.

As Basil says, you should probably use DateTimeFormatter.ISO_INSTANT as your format, which results in all successes and more accurate results:

Success: 2021-06-22T18:27:03.5577Z      {InstantSeconds=1624386423, MilliOfSecond=557, MicroOfSecond=557700, NanoOfSecond=557700000},ISO
Success: 2021-06-22T18:27:03.55770Z     {InstantSeconds=1624386423, MilliOfSecond=557, MicroOfSecond=557700, NanoOfSecond=557700000},ISO
Success: 2021-06-22T18:27:03.557700Z        {InstantSeconds=1624386423, MilliOfSecond=557, MicroOfSecond=557700, NanoOfSecond=557700000},ISO
Success: 2021-06-22T18:27:03.5577000Z       {InstantSeconds=1624386423, MilliOfSecond=557, MicroOfSecond=557700, NanoOfSecond=557700000},ISO
Success: 2021-06-22T18:27:03.55770000Z      {InstantSeconds=1624386423, MilliOfSecond=557, MicroOfSecond=557700, NanoOfSecond=557700000},ISO

The real question though is, "What is expected of this formatter"? It seems like it's meant to be parsing instants but without documentation we're not sure. Is it really meant to fail on certain date formats which are supported by instant, is it really meant to be expecting a literal 'Z' rather than the offset? If it's intent is to be able to parse these instants accurately then changing it to actually be accurate is what you should do.

BUT If it's meant to be more restrictive and fail in weird cases then using aksappy's idea might be the way to go, since his format keeps close to your existing format but allows you to explicitly add to the pattern to match new formats you want to match.


I expect that your success with six digits after the decimal is a bug in your java.time or whichever library is providing this API.

Running with OpenJDK 16.0.1:

public static void main(String[] args) {
    DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'");
    String[] dates = {
            "2021-06-22T18:27:03.5577Z",
            "2021-06-22T18:27:03.55770Z",
            "2021-06-22T18:27:03.557700Z",
            "2021-06-22T18:27:03.5577000Z", 
            "2021-06-22T18:27:03.55770000Z",
    };
    for (String date : dates) {
        try {
            System.out.println("Success: " + date + "\t\t" + TIME_FORMATTER.parse(date));
        } catch (Exception e) {
            System.out.println("Failure: " + date);
        }
    }
}

I get the following results that match what I expect based on your format:

Failure: 2021-06-22T18:27:03.5577Z      Text '2021-06-22T18:27:03.5577Z' could not be parsed at index 20
Failure: 2021-06-22T18:27:03.55770Z     Text '2021-06-22T18:27:03.55770Z' could not be parsed at index 20
Failure: 2021-06-22T18:27:03.557700Z        Text '2021-06-22T18:27:03.557700Z' could not be parsed at index 20
Success: 2021-06-22T18:27:03.5577000Z       {},ISO resolved to 2021-06-22T18:27:03.557700
Failure: 2021-06-22T18:27:03.55770000Z      Text '2021-06-22T18:27:03.55770000Z' could not be parsed at index 27

If you want to make sure that you pass date only in UTC timezone and only with 'Z' literal and if fail is expexcted result if timezone is different this code is working solution:

    new DateTimeFormatterBuilder()
        .append(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
        .appendFraction(ChronoField.MICRO_OF_SECOND, 4, 7, true)
        .appendLiteral("Z")
        .toFormatter().parse("2021-06-22T18:27:03.5577Z");
Related