JDK-8177021 : Parsing with DateTimeFormatter.withZone does not behave as described in javadocs
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.time
  • Affected Version: 8
  • Priority: P4
  • Status: Resolved
  • Resolution: Cannot Reproduce
  • OS: generic
  • CPU: generic
  • Submitted: 2017-03-09
  • Updated: 2021-10-08
  • Resolved: 2021-10-08
Description
FULL PRODUCT VERSION :
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
ProductName:	Mac OS X
ProductVersion:	10.10.5
BuildVersion:	14F2109

A DESCRIPTION OF THE PROBLEM :
Attempting to parse strings with optional time zones, using the behavior of DateTimeFormatter.withZone described in this paragraph of it's javadocs:

---
When parsing, there are two distinct cases to consider. If a zone has been parsed directly from the text, perhaps because DateTimeFormatterBuilder.appendZoneId() was used, then this override zone has no effect. If no zone has been parsed, then this override zone will be included in the result of the parse where it can be used to build instants and date-times.
---

However, I am observing completely different behavior:
If I parse an Instant, the override zone is always used, regardless of whether the parsed text contains a zone offset.
If I parse as an OffsetDateTime, the override zone is never used, and the parsing fails if the text does not contain a zone offset.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
See "Source code for an executable test case:" below.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The tests should pass
ACTUAL -
The tests fail at the locations indicated by my comments

ERROR MESSAGES/STACK TRACES THAT OCCUR :
When parsing as an Instant, the offset in the text is ignored, resulting in:
java.lang.AssertionError: expected:<2001-01-01T01:00:00Z> but was:<2001-01-01T02:00:00Z>

When parsing text without an offset as an OffsetDateTime, the override offset is ignored, resulting in:
java.time.format.DateTimeParseException: Text '2001-01-01T01:00' could not be parsed: Unable to obtain OffsetDateTime from TemporalAccessor: {InstantSeconds=978310800},ISO,Z resolved to 2001-01-01T01:00 of type java.time.format.Parsed


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import org.junit.Test;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;

import static org.junit.Assert.assertEquals;

/**
 * Testing the following paragraph from {@link DateTimeFormatter#withZone(ZoneId)}:
 * <p>
 * When parsing, there are two distinct cases to consider.
 * If a zone has been parsed directly from the text, perhaps because
 * {@link DateTimeFormatterBuilder#appendZoneId()} was used, then
 * this override zone has no effect.
 * If no zone has been parsed, then this override zone will be included in
 * the result of the parse where it can be used to build instants and date-times.
 */
public class ParseWithOptionalZoneTest {
	// Parser with optional time zone, which should use UTC if zone is missing
	DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm[Z]").withZone(ZoneOffset.UTC);
	// These two strings represent the same instant in time
	String withoutTz = "2001-01-01T01:00";
	String withTz    = "2001-01-01T02:00+0100";
	// And this is the correct Instant that they represent
	Instant expected = Instant.parse("2001-01-01T01:00:00Z");

	@Test
	public void testParseInstantWithOptionalZone() {
		// This passes
		assertEquals(expected, parser.parse(withoutTz, Instant::from));
		// This fails (the +01:00 offset is ignored, and the override zone of UTC is used instead)
		assertEquals(expected, parser.parse(withTz, Instant::from));
		// java.lang.AssertionError: expected:<2001-01-01T01:00:00Z> but was:<2001-01-01T02:00:00Z>
	}

	@Test
	public void testParseOffsetDateTimeWithOptionalZone() {
		// This passes (Parsing as OffsetDateTime correctly preserves the offset from the string)
		assertEquals(expected, parser.parse(withTz, OffsetDateTime::from).toInstant());
		// This fails (the override zone isn't used)
		assertEquals(expected, parser.parse(withoutTz, OffsetDateTime::from).toInstant());
		// java.time.format.DateTimeParseException: Text '2001-01-01T01:00' could not be parsed: Unable to obtain OffsetDateTime from TemporalAccessor: {InstantSeconds=978310800},ISO,Z resolved to 2001-01-01T01:00 of type java.time.format.Parsed
	}
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
import org.junit.Test;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalQuery;

import static org.junit.Assert.assertEquals;

public class ParseWithOptionalZoneWorkaroundTest {
	// These two strings represent the same instant in time
	String withoutTz = "2001-01-01T01:00";
	String withTz    = "2001-01-01T02:00+0100";
	// And this is the correct Instant that they represent
	Instant expected = Instant.parse("2001-01-01T01:00:00Z");

	@Test
	public void workaround() {
		// These pass
		assertEquals(expected, parseWorkaround(withTz, Instant::from));
		assertEquals(expected, parseWorkaround(withoutTz, OffsetDateTime::from).toInstant());
	}

	DateTimeFormatter parserWithTz = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mmZ");
	DateTimeFormatter parserWithoutTz = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm");
	private <T> T parseWorkaround(String text, TemporalQuery<T> query) {
		try {
			return parserWithTz.parse(text, query);
		} catch (Exception e) {
			// Parse as LocalDateTime and then add a time zone of UTC.
			return query.queryFrom(parserWithoutTz.parse(text, LocalDateTime::from).atZone(ZoneOffset.UTC));
		}
	}
}



Comments
Cannot reproduce with JDK17. Closing this issue as such.
08-10-2021

To reproduce the issue, run the attached test case. It fails on JDK 8u121 but passes in JDK 9-ea. Following are the results : JDK 8 - Fail JDK 8u121 - Fail JDK 9-ea + 157 - Pass Following is the output on JDK 8: JUnit version 4.4 .E.E Time: 0.312 There were 2 failures: 1) testParseOffsetDateTimeWithOptionalZone(JI9048046) java.time.format.DateTimeParseException: Text '2001-01-01T01:00' could not be parsed: Unable to obtain OffsetDateTime from TemporalAccessor: {InstantSeconds=978310800},ISO,Z resolved to 2001-01-01T01:00 of type java.time.format.Parsed at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1920) at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1855) at JI9048046.testParseOffsetDateTimeWithOptionalZone(JI9048046.java:31) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59) at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98) at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79) at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87) at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77) at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42) at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88) at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51) at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44) at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27) at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37) at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42) at org.junit.internal.runners.CompositeRunner.runChildren(CompositeRunner.java:33) at org.junit.internal.runners.CompositeRunner.run(CompositeRunner.java:28) at org.junit.runner.JUnitCore.run(JUnitCore.java:130) at org.junit.runner.JUnitCore.run(JUnitCore.java:109) at org.junit.runner.JUnitCore.run(JUnitCore.java:100) at org.junit.runner.JUnitCore.runMain(JUnitCore.java:81) at org.junit.runner.JUnitCore.main(JUnitCore.java:44) Caused by: java.time.DateTimeException: Unable to obtain OffsetDateTime from TemporalAccessor: {InstantSeconds=978310800},ISO,Z resolved to 2001-01-01T01:00 of type java.time.format.Parsed at java.time.OffsetDateTime.from(OffsetDateTime.java:370) at java.time.format.Parsed.query(Parsed.java:226) at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851) ... 24 more Caused by: java.time.DateTimeException: Unable to obtain ZoneOffset from TemporalAccessor: {InstantSeconds=978310800},ISO,Z resolved to 2001-01-01T01:00 of type java.time.format.Parsed at java.time.ZoneOffset.from(ZoneOffset.java:348) at java.time.OffsetDateTime.from(OffsetDateTime.java:359) ... 26 more 2) testParseInstantWithOptionalZone(JI9048046) java.lang.AssertionError: expected:<2001-01-01T01:00:00Z> but was:<2001-01-01T02:00:00Z> at org.junit.Assert.fail(Assert.java:74) at org.junit.Assert.failNotEquals(Assert.java:448) at org.junit.Assert.assertEquals(Assert.java:102) at org.junit.Assert.assertEquals(Assert.java:117) at JI9048046.testParseInstantWithOptionalZone(JI9048046.java:22) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59) at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98) at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79) at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87) at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77) at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42) at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88) at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51) at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44) at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27) at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37) at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42) at org.junit.internal.runners.CompositeRunner.runChildren(CompositeRunner.java:33) at org.junit.internal.runners.CompositeRunner.run(CompositeRunner.java:28) at org.junit.runner.JUnitCore.run(JUnitCore.java:130) at org.junit.runner.JUnitCore.run(JUnitCore.java:109) at org.junit.runner.JUnitCore.run(JUnitCore.java:100) at org.junit.runner.JUnitCore.runMain(JUnitCore.java:81) at org.junit.runner.JUnitCore.main(JUnitCore.java:44) FAILURES!!! Tests run: 2, Failures: 2 Output on JDK 9-ea: JUnit version 4.4 .. Time: 0.078 OK (2 tests)
17-03-2017