8032051: "ZonedDateTime" class "parse" method fails with short time zone offset ("+01")
authorntv
Mon, 21 Mar 2016 14:24:11 -0400
changeset 36646 de0663d2d82e
parent 36645 dd51a5761eec
child 36647 26bac33028c6
8032051: "ZonedDateTime" class "parse" method fails with short time zone offset ("+01") Reviewed-by: rriggs, scolebourne
jdk/src/java.base/share/classes/java/time/format/DateTimeFormatter.java
jdk/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java
jdk/test/java/time/tck/java/time/TCKZonedDateTime.java
jdk/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java
jdk/test/java/time/tck/java/time/format/TCKOffsetPrinterParser.java
--- a/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatter.java	Mon Mar 21 08:48:34 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatter.java	Mon Mar 21 14:24:11 2016 -0400
@@ -932,6 +932,7 @@
      * <li>The {@link #ISO_LOCAL_DATE_TIME}
      * <li>The {@link ZoneOffset#getId() offset ID}. If the offset has seconds then
      *  they will be handled even though this is not part of the ISO-8601 standard.
+     *  The offset parsing is lenient, which allows the minutes and seconds to be optional.
      *  Parsing is case insensitive.
      * </ul>
      * <p>
@@ -944,7 +945,9 @@
         ISO_OFFSET_DATE_TIME = new DateTimeFormatterBuilder()
                 .parseCaseInsensitive()
                 .append(ISO_LOCAL_DATE_TIME)
+                .parseLenient()
                 .appendOffsetId()
+                .parseStrict()
                 .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
     }
 
@@ -1169,6 +1172,7 @@
      * <li>If the offset is not available to format or parse then the format is complete.
      * <li>The {@link ZoneOffset#getId() offset ID} without colons. If the offset has
      *  seconds then they will be handled even though this is not part of the ISO-8601 standard.
+     *  The offset parsing is lenient, which allows the minutes and seconds to be optional.
      *  Parsing is case insensitive.
      * </ul>
      * <p>
@@ -1187,7 +1191,9 @@
                 .appendValue(MONTH_OF_YEAR, 2)
                 .appendValue(DAY_OF_MONTH, 2)
                 .optionalStart()
+                .parseLenient()
                 .appendOffset("+HHMMss", "Z")
+                .parseStrict()
                 .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE);
     }
 
--- a/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java	Mon Mar 21 08:48:34 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java	Mon Mar 21 14:24:11 2016 -0400
@@ -866,7 +866,9 @@
      * Appends the zone offset, such as '+01:00', to the formatter.
      * <p>
      * This appends an instruction to format/parse the offset ID to the builder.
-     * This is equivalent to calling {@code appendOffset("+HH:MM:ss", "Z")}.
+     * This is equivalent to calling {@code appendOffset("+HH:mm:ss", "Z")}.
+     * See {@link #appendOffset(String, String)} for details on formatting
+     * and parsing.
      *
      * @return this, for chaining, not null
      */
@@ -886,9 +888,18 @@
      * If the offset cannot be obtained then an exception is thrown unless the
      * section of the formatter is optional.
      * <p>
-     * During parsing, the offset is parsed using the format defined below.
-     * If the offset cannot be parsed then an exception is thrown unless the
-     * section of the formatter is optional.
+     * When parsing in strict mode, the input must contain the mandatory
+     * and optional elements are defined by the specified pattern.
+     * If the offset cannot be parsed then an exception is thrown unless
+     * the section of the formatter is optional.
+     * <p>
+     * When parsing in lenient mode, only the hours are mandatory - minutes
+     * and seconds are optional. The colons are required if the specified
+     * pattern contains a colon. If the specified pattern is "+HH", the
+     * presence of colons is determined by whether the character after the
+     * hour digits is a colon or not.
+     * If the offset cannot be parsed then an exception is thrown unless
+     * the section of the formatter is optional.
      * <p>
      * The format of the offset is controlled by a pattern which must be one
      * of the following:
@@ -902,6 +913,10 @@
      * <li>{@code +HH:MM:ss} - hour and minute, with second if non-zero, with colon
      * <li>{@code +HHMMSS} - hour, minute and second, no colon
      * <li>{@code +HH:MM:SS} - hour, minute and second, with colon
+     * <li>{@code +HHmmss} - hour, with minute if non-zero or with minute and
+     * second if non-zero, no colon
+     * <li>{@code +HH:mm:ss} - hour, with minute if non-zero or with minute and
+     * second if non-zero, with colon
      * </ul>
      * The "no offset" text controls what text is printed when the total amount of
      * the offset fields to be output is zero.
@@ -3318,7 +3333,7 @@
      */
     static final class OffsetIdPrinterParser implements DateTimePrinterParser {
         static final String[] PATTERNS = new String[] {
-            "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS",
+            "+HH", "+HHmm", "+HH:mm", "+HHMM", "+HH:MM", "+HHMMss", "+HH:MM:ss", "+HHMMSS", "+HH:MM:SS", "+HHmmss", "+HH:mm:ss",
         };  // order used in pattern builder
         static final OffsetIdPrinterParser INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z");
         static final OffsetIdPrinterParser INSTANCE_ID_ZERO = new OffsetIdPrinterParser("+HH:MM:ss", "0");
@@ -3365,11 +3380,11 @@
                 int output = absHours;
                 buf.append(totalSecs < 0 ? "-" : "+")
                     .append((char) (absHours / 10 + '0')).append((char) (absHours % 10 + '0'));
-                if (type >= 3 || (type >= 1 && absMinutes > 0)) {
+                if ((type >= 3 && type < 9) || (type >= 9 && absSeconds > 0) || (type >= 1 && absMinutes > 0)) {
                     buf.append((type % 2) == 0 ? ":" : "")
                         .append((char) (absMinutes / 10 + '0')).append((char) (absMinutes % 10 + '0'));
                     output += absMinutes;
-                    if (type >= 7 || (type >= 5 && absSeconds > 0)) {
+                    if (type == 7  || type == 8 || (type >= 5 && absSeconds > 0)) {
                         buf.append((type % 2) == 0 ? ":" : "")
                             .append((char) (absSeconds / 10 + '0')).append((char) (absSeconds % 10 + '0'));
                         output += absSeconds;
@@ -3387,6 +3402,15 @@
         public int parse(DateTimeParseContext context, CharSequence text, int position) {
             int length = text.length();
             int noOffsetLen = noOffsetText.length();
+            int parseType = type;
+            if (context.isStrict() == false) {
+                if ((parseType > 0 && (parseType % 2) == 0) ||
+                    (parseType == 0 && length > position + 3 && text.charAt(position + 3) == ':')) {
+                    parseType = 10;
+                } else {
+                    parseType = 9;
+                }
+            }
             if (noOffsetLen == 0) {
                 if (position == length) {
                     return context.setParsedField(OFFSET_SECONDS, 0, position, position);
@@ -3407,9 +3431,9 @@
                 int negative = (sign == '-' ? -1 : 1);
                 int[] array = new int[4];
                 array[0] = position + 1;
-                if ((parseNumber(array, 1, text, true) ||
-                        parseNumber(array, 2, text, type >=3) ||
-                        parseNumber(array, 3, text, false)) == false) {
+                if ((parseNumber(array, 1, text, true, parseType) ||
+                        parseNumber(array, 2, text, parseType >= 3 && parseType < 9, parseType) ||
+                        parseNumber(array, 3, text, parseType == 7 || parseType == 8, parseType)) == false) {
                     // success
                     long offsetSecs = negative * (array[1] * 3600L + array[2] * 60L + array[3]);
                     return context.setParsedField(OFFSET_SECONDS, offsetSecs, position, array[0]);
@@ -3417,7 +3441,7 @@
             }
             // handle special case of empty no offset text
             if (noOffsetLen == 0) {
-                return context.setParsedField(OFFSET_SECONDS, 0, position, position + noOffsetLen);
+                return context.setParsedField(OFFSET_SECONDS, 0, position, position);
             }
             return ~position;
         }
@@ -3429,14 +3453,15 @@
          * @param arrayIndex  the index to parse the value into
          * @param parseText  the offset ID, not null
          * @param required  whether this number is required
+         * @param parseType the offset pattern type
          * @return true if an error occurred
          */
-        private boolean parseNumber(int[] array, int arrayIndex, CharSequence parseText, boolean required) {
-            if ((type + 3) / 2 < arrayIndex) {
+        private boolean parseNumber(int[] array, int arrayIndex, CharSequence parseText, boolean required, int parseType) {
+            if ((parseType + 3) / 2 < arrayIndex) {
                 return false;  // ignore seconds/minutes
             }
             int pos = array[0];
-            if ((type % 2) == 0 && arrayIndex > 1) {
+            if ((parseType % 2) == 0 && arrayIndex > 1) {
                 if (pos + 1 > parseText.length() || parseText.charAt(pos) != ':') {
                     return required;
                 }
--- a/jdk/test/java/time/tck/java/time/TCKZonedDateTime.java	Mon Mar 21 08:48:34 2016 -0700
+++ b/jdk/test/java/time/tck/java/time/TCKZonedDateTime.java	Mon Mar 21 14:24:11 2016 -0400
@@ -761,6 +761,7 @@
                 {"2012-06-30T12:30:40-01:00[UT-01:00]", 2012, 6, 30, 12, 30, 40, 0, "UT-01:00"},
                 {"2012-06-30T12:30:40-01:00[UTC-01:00]", 2012, 6, 30, 12, 30, 40, 0, "UTC-01:00"},
                 {"2012-06-30T12:30:40+01:00[Europe/London]", 2012, 6, 30, 12, 30, 40, 0, "Europe/London"},
+                {"2012-06-30T12:30:40+01", 2012, 6, 30, 12, 30, 40, 0, "+01:00"},
         };
     }
 
--- a/jdk/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java	Mon Mar 21 08:48:34 2016 -0700
+++ b/jdk/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java	Mon Mar 21 14:24:11 2016 -0400
@@ -59,11 +59,13 @@
  */
 package tck.java.time.format;
 
+import static java.time.format.DateTimeFormatter.BASIC_ISO_DATE;
 import static java.time.temporal.ChronoField.DAY_OF_MONTH;
 import static java.time.temporal.ChronoField.HOUR_OF_DAY;
 import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
 import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
 import static java.time.temporal.ChronoField.NANO_OF_SECOND;
+import static java.time.temporal.ChronoField.OFFSET_SECONDS;
 import static java.time.temporal.ChronoField.YEAR;
 import static org.testng.Assert.assertEquals;
 
@@ -73,6 +75,7 @@
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
 import java.time.format.SignStyle;
 import java.time.format.TextStyle;
 import java.time.temporal.Temporal;
@@ -339,6 +342,18 @@
                 {"+HH", 2, 0, 45, "+02"},
                 {"+HH", 2, 30, 45, "+02"},
 
+                {"+HHmm", 2, 0, 0, "+02"},
+                {"+HHmm", -2, 0, 0, "-02"},
+                {"+HHmm", 2, 30, 0, "+0230"},
+                {"+HHmm", 2, 0, 45, "+02"},
+                {"+HHmm", 2, 30, 45, "+0230"},
+
+                {"+HH:mm", 2, 0, 0, "+02"},
+                {"+HH:mm", -2, 0, 0, "-02"},
+                {"+HH:mm", 2, 30, 0, "+02:30"},
+                {"+HH:mm", 2, 0, 45, "+02"},
+                {"+HH:mm", 2, 30, 45, "+02:30"},
+
                 {"+HHMM", 2, 0, 0, "+0200"},
                 {"+HHMM", -2, 0, 0, "-0200"},
                 {"+HHMM", 2, 30, 0, "+0230"},
@@ -374,6 +389,20 @@
                 {"+HH:MM:SS", 2, 30, 0, "+02:30:00"},
                 {"+HH:MM:SS", 2, 0, 45, "+02:00:45"},
                 {"+HH:MM:SS", 2, 30, 45, "+02:30:45"},
+
+                {"+HHmmss", 2, 0, 0, "+02"},
+                {"+HHmmss", -2, 0, 0, "-02"},
+                {"+HHmmss", 2, 30, 0, "+0230"},
+                {"+HHmmss", 2, 0, 45, "+020045"},
+                {"+HHmmss", 2, 30, 45, "+023045"},
+
+                {"+HH:mm:ss", 2, 0, 0, "+02"},
+                {"+HH:mm:ss", -2, 0, 0, "-02"},
+                {"+HH:mm:ss", 2, 30, 0, "+02:30"},
+                {"+HH:mm:ss", 2, 0, 45, "+02:00:45"},
+                {"+HH:mm:ss", 2, 30, 45, "+02:30:45"},
+
+
         };
     }
 
@@ -878,4 +907,82 @@
         assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
     }
 
+    @DataProvider(name="lenientOffsetParseData")
+    Object[][] data_lenient_offset_parse() {
+        return new Object[][] {
+            {"+HH", "+01", 3600},
+            {"+HH", "+0101", 3660},
+            {"+HH", "+010101", 3661},
+            {"+HH", "+01", 3600},
+            {"+HH", "+01:01", 3660},
+            {"+HH", "+01:01:01", 3661},
+            {"+HHmm", "+01", 3600},
+            {"+HHmm", "+0101", 3660},
+            {"+HHmm", "+010101", 3661},
+            {"+HH:mm", "+01", 3600},
+            {"+HH:mm", "+01:01", 3660},
+            {"+HH:mm", "+01:01:01", 3661},
+            {"+HHMM", "+01", 3600},
+            {"+HHMM", "+0101", 3660},
+            {"+HHMM", "+010101", 3661},
+            {"+HH:MM", "+01", 3600},
+            {"+HH:MM", "+01:01", 3660},
+            {"+HH:MM", "+01:01:01", 3661},
+            {"+HHMMss", "+01", 3600},
+            {"+HHMMss", "+0101", 3660},
+            {"+HHMMss", "+010101", 3661},
+            {"+HH:MM:ss", "+01", 3600},
+            {"+HH:MM:ss", "+01:01", 3660},
+            {"+HH:MM:ss", "+01:01:01", 3661},
+            {"+HHMMSS", "+01", 3600},
+            {"+HHMMSS", "+0101", 3660},
+            {"+HHMMSS", "+010101", 3661},
+            {"+HH:MM:SS", "+01", 3600},
+            {"+HH:MM:SS", "+01:01", 3660},
+            {"+HH:MM:SS", "+01:01:01", 3661},
+            {"+HHmmss", "+01", 3600},
+            {"+HHmmss", "+0101", 3660},
+            {"+HHmmss", "+010101", 3661},
+            {"+HH:mm:ss", "+01", 3600},
+            {"+HH:mm:ss", "+01:01", 3660},
+            {"+HH:mm:ss", "+01:01:01", 3661},
+        };
+    }
+
+    @Test(dataProvider="lenientOffsetParseData")
+    public void test_lenient_offset_parse_1(String pattern, String offset, int offsetSeconds) {
+        assertEquals(new DateTimeFormatterBuilder().parseLenient().appendOffset(pattern, "Z").toFormatter().parse(offset).get(OFFSET_SECONDS),
+                     offsetSeconds);
+    }
+
+    @Test
+    public void test_lenient_offset_parse_2() {
+        assertEquals(new DateTimeFormatterBuilder().parseLenient().appendOffsetId().toFormatter().parse("+01").get(OFFSET_SECONDS),
+                     3600);
+    }
+
+    @Test(expectedExceptions=DateTimeParseException.class)
+    public void test_strict_appendOffsetId() {
+        assertEquals(new DateTimeFormatterBuilder().appendOffsetId().toFormatter().parse("+01").get(OFFSET_SECONDS),
+                     3600);
+    }
+
+    @Test(expectedExceptions=DateTimeParseException.class)
+    public void test_strict_appendOffset_1() {
+        assertEquals(new DateTimeFormatterBuilder().appendOffset("+HH:MM:ss", "Z").toFormatter().parse("+01").get(OFFSET_SECONDS),
+                     3600);
+    }
+
+    @Test(expectedExceptions=DateTimeParseException.class)
+    public void test_strict_appendOffset_2() {
+        assertEquals(new DateTimeFormatterBuilder().appendOffset("+HHMMss", "Z").toFormatter().parse("+01").get(OFFSET_SECONDS),
+                     3600);
+    }
+
+    @Test
+    public void test_basic_iso_date() {
+        assertEquals(BASIC_ISO_DATE.parse("20021231+01").get(OFFSET_SECONDS), 3600);
+        assertEquals(BASIC_ISO_DATE.parse("20021231+0101").get(OFFSET_SECONDS), 3660);
+    }
+
 }
--- a/jdk/test/java/time/tck/java/time/format/TCKOffsetPrinterParser.java	Mon Mar 21 08:48:34 2016 -0700
+++ b/jdk/test/java/time/tck/java/time/format/TCKOffsetPrinterParser.java	Mon Mar 21 14:24:11 2016 -0400
@@ -199,6 +199,30 @@
                 {"+HH:MM:SS", "Z", DT_2012_06_30_12_30_40, OFFSET_M0023, "-00:23:00"},
                 {"+HH:MM:SS", "Z", DT_2012_06_30_12_30_40, OFFSET_M012345, "-01:23:45"},
                 {"+HH:MM:SS", "Z", DT_2012_06_30_12_30_40, OFFSET_M000045, "-00:00:45"},
+
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_UTC, "Z"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_P0100, "+01"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_P0123, "+01:23"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_P0023, "+00:23"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_P012345, "+01:23:45"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_M000045, "-00:00:45"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_M0100, "-01"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_M0123, "-01:23"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_M0023, "-00:23"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_M012345, "-01:23:45"},
+                {"+HH:mm:ss", "Z", DT_2012_06_30_12_30_40, OFFSET_M000045, "-00:00:45"},
+
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_UTC, "Z"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_P0100, "+01"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_P0123, "+0123"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_P0023, "+0023"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_P012345, "+012345"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_P000045, "+000045"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_M0100, "-01"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_M0123, "-0123"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_M0023, "-0023"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_M012345, "-012345"},
+                {"+HHmmss", "Z", DT_2012_06_30_12_30_40, OFFSET_M000045, "-000045"},
         };
     }