--- old/src/java.base/share/classes/java/time/format/DateTimeFormatter.java 2016-02-25 18:40:41.226671501 +0300 +++ new/src/java.base/share/classes/java/time/format/DateTimeFormatter.java 2016-02-25 18:40:39.185671501 +0300 @@ -923,6 +923,7 @@ *
  • The {@link #ISO_LOCAL_DATE_TIME} *
  • 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. * *

    @@ -935,7 +936,9 @@ ISO_OFFSET_DATE_TIME = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(ISO_LOCAL_DATE_TIME) + .parseLenient() .appendOffsetId() + .parseStrict() .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE); } @@ -1160,6 +1163,7 @@ *

  • If the offset is not available to format or parse then the format is complete. *
  • 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. * *

    @@ -1178,7 +1182,9 @@ .appendValue(MONTH_OF_YEAR, 2) .appendValue(DAY_OF_MONTH, 2) .optionalStart() + .parseLenient() .appendOffset("+HHMMss", "Z") + .parseStrict() .toFormatter(ResolverStyle.STRICT, IsoChronology.INSTANCE); } --- old/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java 2016-02-25 18:40:42.497671501 +0300 +++ new/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java 2016-02-25 18:40:42.127671501 +0300 @@ -866,7 +866,9 @@ * Appends the zone offset, such as '+01:00', to the formatter. *

    * 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,19 @@ * If the offset cannot be obtained then an exception is thrown unless the * section of the formatter is optional. *

    - * 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. + *

    + * 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. *

    * The format of the offset is controlled by a pattern which must be one * of the following: @@ -902,6 +914,10 @@ *

  • {@code +HH:MM:ss} - hour and minute, with second if non-zero, with colon *
  • {@code +HHMMSS} - hour, minute and second, no colon *
  • {@code +HH:MM:SS} - hour, minute and second, with colon + *
  • {@code +HHmmss} - hour, with minute if non-zero or with minute and + * second if non-zero, no colon + *
  • {@code +HH:mm:ss} - hour, with minute if non-zero or with minute and + * second if non-zero, with colon * * The "no offset" text controls what text is printed when the total amount of * the offset fields to be output is zero. @@ -3315,7 +3331,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"); @@ -3362,11 +3378,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; @@ -3384,6 +3400,13 @@ public int parse(DateTimeParseContext context, CharSequence text, int position) { int length = text.length(); int noOffsetLen = noOffsetText.length(); + int parseType = type; + if (context.isStrict() == false) { + parseType = 9; + if ((length > position + 3) && (text.charAt(position + 3) == ':')) { + parseType = 10; + } + } if (noOffsetLen == 0) { if (position == length) { return context.setParsedField(OFFSET_SECONDS, 0, position, position); @@ -3404,9 +3427,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]); @@ -3414,7 +3437,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; } @@ -3426,14 +3449,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; } --- old/test/java/time/tck/java/time/TCKZonedDateTime.java 2016-02-25 18:40:43.616671501 +0300 +++ new/test/java/time/tck/java/time/TCKZonedDateTime.java 2016-02-25 18:40:43.139671501 +0300 @@ -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"}, }; } --- old/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java 2016-02-25 18:40:44.346671501 +0300 +++ new/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java 2016-02-25 18:40:44.092671501 +0300 @@ -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); + } + } --- old/test/java/time/tck/java/time/format/TCKOffsetPrinterParser.java 2016-02-25 18:40:45.044671501 +0300 +++ new/test/java/time/tck/java/time/format/TCKOffsetPrinterParser.java 2016-02-25 18:40:44.760671501 +0300 @@ -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"}, }; }