This fails.
OffsetDateTime.now()
.format(
DateTimeFormatter.ofLocalizedDateTime( FormatStyle.LONG )
) // throws DateTimeException.
But the same moment with same offset in a ZonedDateTime works.
Why?
When letting java.time automatically localize the string representation of a OffsetDateTime via DateTimeFormatter.ofLocalizedDateTime, calling format works if the formatter carries a FormatStyle of SHORT or MEDIUM. But when the formatter carries LONG or FULL, a DateTimeException is thrown. Yet ZonedDateTime succeeds using the same moment with the same offset. Why?
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.LONG ) ;
OffsetDateTime odt = OffsetDateTime.now( ZoneId.systemDefault() ) ;
ZonedDateTime zdt = odt.atZoneSameInstant( odt.getOffset() ) ; // Generate a `ZonedDateTime` with same moment and same offset as the `OffsetDateTime`.
// Succeeds.
String outputZdt = zdt.format( f ) ;
System.out.println( "outputZdt: " + outputZdt ) ;
// Fails. Throws exception.
if ( false ) {
String outputOdt = odt.format( f ) ; // Throws exception.
System.out.println( "outputOdt: " + outputOdt ) ;
}
See this code run live at IdeOne.com.
When run…
The good.
outputZdt: September 16, 2017 8:42:14 AM Z
The bad.
Exception in thread "main" java.time.DateTimeException: Unable to extract value: class java.time.OffsetDateTime
at java.time.format.DateTimePrintContext.getValue(DateTimePrintContext.java:282)
at java.time.format.DateTimeFormatterBuilder$ZoneTextPrinterParser.format(DateTimeFormatterBuilder.java:3682)
at java.time.format.DateTimeFormatterBuilder$CompositePrinterParser.format(DateTimeFormatterBuilder.java:2179)
at java.time.format.DateTimeFormatterBuilder$LocalizedPrinterParser.format(DateTimeFormatterBuilder.java:4347)
at java.time.format.DateTimeFormatterBuilder$CompositePrinterParser.format(DateTimeFormatterBuilder.java:2179)
at java.time.format.DateTimeFormatter.formatTo(DateTimeFormatter.java:1746)
at java.time.format.DateTimeFormatter.format(DateTimeFormatter.java:1720)
at java.time.OffsetDateTime.format(OffsetDateTime.java:1674)
at Ideone.main(Main.java:28)
I wrote the core of that code to work around the exception thrown, odt.atZoneSameInstant( odt.getOffset() ). Then I realized, why doesn’t java.time do the same thing internally? Why should the OffsetDateTime fail to format where a ZonedDateTime with the same moment and the same offset succeeds? Why should I need to do this conversion from OffsetDateTime to ZonedDateTime?
➟ Is this behavior of OffsetDateTime formatting failure a bug or a feature?
I would file a bug report, but I want to make sure I am misunderstanding something.
Looks like the Javadoc bug reported here. In the example provided they use LocalDateTime, but the behavior is the same.
The use of FormatStyle.LONG and FormatStyle.FULL seems to require a ZoneId which OffsetDateTime does not have
Please review
java.timejavadoc improvements to highlight a common misunderstanding about formatting elements that require a timezone in addition to the time.When using locale specific formatting, it may work if the locale formatting does not require a timezone or fail if the locale formatting requires a timezone and a timezone is not provided.
which is why they clarified the javadoc to mention
* The {@code FULL} and {@code LONG} styles typically require a time-zone.
* When formatting using these styles, a {@code ZoneId} must be available,
* either by using {@code ZonedDateTime} or {@link DateTimeFormatter#withZone}.
You could create the DateTimeFormatter with the OffsetDateTime's ZoneOffset.
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
.withZone(odt.getOffset());
in which case the OffsetDateTime will be converted to a ZonedDateTime before formatting occurs.
Debugging the code, I've found that the formatter ends up in this line (grepcode's line is not exactly the same number of my JDK installation, but the code is):
ZoneId zone = context.getValue(TemporalQueries.zoneId());
It tries to extract the zone, using the built-in query TemporalQueries.zoneId(). According to javadoc, this query returns null if the temporal object is an OffsetDateTime:
Thus a ZonedDateTime will return the result of getZone(), but an OffsetDateTime will return null.
You can confirm this by calling odt.query(TemporalQueries.zoneId()) - it returns null indeed.
Later, the result of this query is checked by a DateTimePrintContext:
R result = temporal.query(query);
if (result == null && optional == 0) {
throw new DateTimeException("Unable to extract value: " + temporal.getClass());
}
As the result is null, it throws the exception.
Actually, trying to get the zone name (pattern z) from an OffsetDateTime will throw an exception:
// java.time.DateTimeException: Unable to extract value: class java.time.OffsetDateTime
DateTimeFormatter.ofPattern("z").format(OffsetDateTime.now());
Because this pattern ends up in the problematic line described above.
And checking the date style for all locales, using getLocalizedDateTimePattern:
// did this for all locales
DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.LONG, FormatStyle.LONG,
IsoChronology.INSTANCE, locale);
I didn't check all, but most of them have the lowercase z pattern, which means that it will fail for most (if not all) locales.
Not directly related, but as you're calling atZoneSameInstant with a ZoneOffset as parameter, you could simply call odt.toZonedDateTime() instead.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With