Does Java's DateTimeFormatter allow lenient parsing for fractional seconds?
Asked Answered
T

1

5

I am currently working with Java's DateTimeFormatter to parse ISO 8601 formatted timestamps, particularly those containing fractional seconds. While experimenting with different timestamp formats, I noticed some unexpected behavior regarding how the formatter handles optional fractional seconds.

Specifically, I am curious about the leniency of the parser when it comes to the number of digits in the fractional seconds. My implementation allows for timestamps with 9 digits for fractional seconds, yet the parser successfully handles timestamps with only 8 digits while failing for those with 7 or fewer. This has led me to wonder if there is an underlying reason for this behavior, whether it is part of the design of the DateTimeFormatter, and if it is documented anywhere.

I wrote a test using the following code:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class DateTimeExample {
    public static void main(String[] args) {
        String[] timestamps = {
            "2023-10-05T15:14:29.123456789Z", // 9 digits
            "2023-10-05T15:14:29.12345678Z",  // 8 digits
            "2023-10-05T15:14:29.1234567Z",   // 7 digits
            "2023-10-05T15:14:29.123456Z",    // 6 digits
            "2023-10-05T15:14:29.12345Z",     // 5 digits
            "2023-10-05T15:14:29.1234Z",      // 4 digits
            "2023-10-05T15:14:29.123Z",       // 3 digits
            "2023-10-05T15:14:29.12Z",        // 2 digits
            "2023-10-05T15:14:29.1Z",         // 1 digit
            "2023-10-05T15:14:29Z"            // no fractional seconds
        };

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

        for (String timestamp : timestamps) {
            try {
                LocalDateTime dateTime = LocalDateTime.parse(timestamp, formatter);
                System.out.println("Parsed date: " + dateTime);
            } catch (DateTimeParseException e) {
                System.err.println("Failed to parse: " + timestamp + " - " + e.getMessage());
            }
        }
    }
}

Observations

When I run this code, this is the output:

Parsed date: 2023-10-05T15:14:29.123456789
Parsed date: 2023-10-05T15:14:29.123456780
Failed to parse: 2023-10-05T15:14:29.1234567Z - Text '2023-10-05T15:14:29.1234567Z' could not be parsed at index 19
Failed to parse: 2023-10-05T15:14:29.123456Z - Text '2023-10-05T15:14:29.123456Z' could not be parsed at index 19
Failed to parse: 2023-10-05T15:14:29.12345Z - Text '2023-10-05T15:14:29.12345Z' could not be parsed at index 19
Failed to parse: 2023-10-05T15:14:29.1234Z - Text '2023-10-05T15:14:29.1234Z' could not be parsed at index 19
Failed to parse: 2023-10-05T15:14:29.123Z - Text '2023-10-05T15:14:29.123Z' could not be parsed at index 19
Failed to parse: 2023-10-05T15:14:29.12Z - Text '2023-10-05T15:14:29.12Z' could not be parsed at index 19
Failed to parse: 2023-10-05T15:14:29.1Z - Text '2023-10-05T15:14:29.1Z' could not be parsed at index 19
Parsed date: 2023-10-05T15:14:29

It successfully parses timestamps with 9 digits for fractional seconds or no fractional part, which is the expected behaviour. But why does it also work with 8 digits for fractional part? My conclusion from this behaviour is that the DateTimeFormatter is lenient with upto one extra digit in the pattern. Is that correct, if so, are there any relevant documentations that I can refer?

Tother answered 3/10 at 10:26 Comment(16)
Which Java version are you using? I cannot reproduce this output on Java 22.Limulus
reproducible (having 8 digits accepted) with Java 8 and Java 11; not with Java 17 onwards (sorry, no other installation below 17 available here)Chittagong
Java 11 and 17. I even tried running this on an online compiler. You can check this here - programiz.com/java-programming/online-compilerTother
@Chittagong are you sure it is not reproducible with Java 17?Tother
pretty sure: screenshotChittagong
hmm, maybe I'm mistaken then. Thanks for verifying.Tother
AFAIK, you need to use the builder, and call setLenient before adding the part of the fractional seconds if you want it to parse varying lengths, or use the appendValue of the builder that specifies the minimum and maximum number of digits.Cowbane
That should probably be DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.n]'Z'");Footle
@g00se, that worked. Thank you. But I'm still confused about this behaviour.Tother
Why? ;) You used the wrong chrono fieldFootle
@g00se, that's not my question. I'm confused why it allows one lesser digit while parsing the timestamp.Tother
@AyushJain Given it no longer happens in Java 17, it was probably a bug.Cowbane
@Footle Using n is not the right format option though, because then in 2023-10-05T15:14:29.1Z the .1 is 1 nanosecond, instead of 100 milliseconds.Cowbane
@MarkRotteveel, but if the OP has between 1 and 9 digits then nanoseconds must be being used surely?Footle
@Footle That depends, but I think that is unlikely. In general, a rendering of ..:29.1 mean 29 seconds and 100 milliseconds (100000000 nanoseconds), ..:29.123 means 29 seconds and 123 milliseconds (123000000 nanoseconds), and ..:29.123456789 means 29 seconds and 123456789 nanoseconds (especially given the OP says they want to use ISO 8601, as that defines them as decimal fractions).Cowbane
I searched briefly and did not find any description of the bug. Nevertheless I think that someone can post an answer saying that we believe it‘s a bug in Java versions up to at least Java 11 (I will be happy to upvote).Palestrina
C
6

It looks like it was a bug in older versions of Java. The bug was reproducible with Java 11 in my system and with Java 12 on IdeOne (currently it uses Java 12). I tested it also with Java 17 in my system but the bug did not appear, as also can be seen in the screenshot shared by user85421.

On a side note,

There are two problems with your code:

  1. Your timestamps have strings with a time zone offset of Z i.e., +00:00. Therefore, you should parse it into an OffsetDateTime rather than a LocalDateTime. Since they are all in ISO 8601 format, you do not need to use a DateTimeFormatter explicitly, as shown in the below demo.
  2. Never specify 'Z' in a date-time parsing/formatting pattern because 'Z' is a character literal while Z is a pattern character specifying time zone offset. To parse a string representing a time zone offset, you must use X (or XX or XXX depending on the requirement).

Demo:

public class Main {
    public static void main(String[] args) {
        System.out.println("Java Version: " + System.getProperty("java.version"));

        String[] timestamps = {
                "2023-10-05T15:14:29.123456789Z", // 9 digits
                "2023-10-05T15:14:29.12345678Z",  // 8 digits
                "2023-10-05T15:14:29.1234567Z",   // 7 digits
                "2023-10-05T15:14:29.123456Z",    // 6 digits
                "2023-10-05T15:14:29.12345Z",     // 5 digits
                "2023-10-05T15:14:29.1234Z",      // 4 digits
                "2023-10-05T15:14:29.123Z",       // 3 digits
                "2023-10-05T15:14:29.12Z",        // 2 digits
                "2023-10-05T15:14:29.1Z",         // 1 digit
                "2023-10-05T15:14:29Z"             // no fractional seconds
        };

        for (String timestamp : timestamps) {
            try {
                System.out.println("Parsed date: " + OffsetDateTime.parse(timestamp));
            } catch (DateTimeParseException e) {
                System.err.println("Failed to parse: " + timestamp + " - " + e.getMessage());
            }
        }
    }
}

Output on my system with Java 17:

Java Version: 17.0.7
Parsed date: 2023-10-05T15:14:29.123456789Z
Parsed date: 2023-10-05T15:14:29.123456780Z
Parsed date: 2023-10-05T15:14:29.123456700Z
Parsed date: 2023-10-05T15:14:29.123456Z
Parsed date: 2023-10-05T15:14:29.123450Z
Parsed date: 2023-10-05T15:14:29.123400Z
Parsed date: 2023-10-05T15:14:29.123Z
Parsed date: 2023-10-05T15:14:29.120Z
Parsed date: 2023-10-05T15:14:29.100Z
Parsed date: 2023-10-05T15:14:29Z

Online Demo

For learners: Learn more about the modern date-time API from Trail: Date Time.

Carmon answered 6/11 at 23:4 Comment(1)
Good work. FYI, you can get the Java version with: Runtime.version() --> 12.0.1+12Businesslike

© 2022 - 2024 — McMap. All rights reserved.