Search code examples
javadatetimeformatterdatetimeparseexception

DateTimeFormatter with parseDefaulting throwing exception


I have

private static final DateTimeFormatter DATE_PATTERN = new DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd[ ]['T'][HH:mm:ss][.SSSSSS][.SSSSS][.SSSS][.SSS][.SS][.S][X]")
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
    .parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
    .toFormatter()
    .withZone(ZoneOffset.UTC); // assume incoming is UTC

used by

public static Function<String, String> getFormattedDate() {
  return dateTime -> {
    try {
      ZonedDateTime timeRemoved =
          ZonedDateTime.parse(dateTime, DATE_PATTERN).truncatedTo(ChronoUnit.DAYS);
      return DateTimeFormatter.ofPattern(DISPLAY_DATE_PATTERN).format(timeRemoved);
    } catch (Exception e) {
      return null;
    }
  };
}

with a test that fails

public void test_data_patter_with_ms(){
  String formattedDate = DateTimeUtil.getFormattedDate().apply("2021-04-24T06:57:06.850");
  assertThat(formattedDate, is("2021-04-24T00:00:00Z"));
}

with the exception

java.time.format.DateTimeParseException: Text '2021-04-24T06:57:06.850' could not be parsed: Conflict found: NanoOfSecond 850000000 differs from NanoOfSecond 0 while resolving MilliOfSecond

If I comment out .parseDefaulting(ChronoField.MILLI_OF_SECOND, 0) it works, but I'm not really sure I understand why and I don't like fixing issues and not understanding the why I fixed it.


Solution

  • Note that the S pattern specifier corresponds to the NANO_OF_SECOND field. Documentation

    Symbol Meaning Presentation Examples
    S fraction-of-second fraction 978

    The count of pattern letters determines the format.

    Fraction: Outputs the nano-of-second field as a fraction-of-second. The nano-of-second value has nine digits, thus the count of pattern letters is from 1 to 9. If it is less than 9, then the nano-of-second value is truncated, with only the most significant digits being output.

    Therefore, when you do parseDefaulting(ChronoField.MILLI_OF_SECOND, 0), the default value 0 is always used. From the documentation of parseDefaulting:

    During parsing, the current state of the parse is inspected. If the specified field has no associated value, because it has not been parsed successfully at that point, then the specified value is injected into the parse result.

    In your case, it is always the case that MILLI_OF_SECOND "has no associated value", because your pattern parses the NANO_OF_SECOND field. There is no MILLI_OF_SECOND component in your pattern in the first place!

    And only then comes the resolution step of the parsing process. This basically figures out the values of unknown fields from the known fields, and also checks that the values of the fields agree with each other. This is where the algorithm finds out that somehow your date has a MILLI_OF_SECOND of 0 due to your parseDefaulting call, but also a NANO_OF_SECOND of 850000000 because of what's in your string. This is a contradiction, and so an exception is thrown.

    I think you should have called parseDefaulting with NANO_OF_SECOND instead:

    .parseDefaulting(ChronoField.NANO_OF_SECOND, 0)
    

    This way, when any of the .SSS options appear in the string, the default value won't be used.

    Side note:

    It is possible to change your pattern so that it parses the MILLI_OF_SECOND field, using appendValue:

    var dtf = new DateTimeFormatterBuilder()
        .appendPattern("yyyy-MM-dd[ ]['T'][HH:mm:ss]")
        .optionalStart()
        .appendLiteral('.')
        .appendValue(ChronoField.MILLI_OF_SECOND, 1, 3, SignStyle.EXCEEDS_PAD)
        .optionalEnd()
        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
        .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
        .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
        .parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
        .toFormatter()
        .withZone(ZoneOffset.UTC);
    
    // note that in this case the milliseconds are not understood as a fraction, but a number
    // 2021-04-24T06:57:06.001Z
    System.out.println(ZonedDateTime.parse("2021-04-24T06:57:06.1", dtf));
    
    // parseDefaulting works
    // 2021-04-24T06:57:06.000Z
    System.out.println(ZonedDateTime.parse("2021-04-24T06:57:06", dtf));