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.
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));