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