Parsing with DateTimeFormatter.withZone does not behave as described in javadocs

XMLWordPrintable

    • generic
    • generic

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


            Assignee:
            Naoto Sato
            Reporter:
            Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: