# HG changeset patch # User rriggs # Date 1381340077 14400 # Node ID 7891146ae1415302947368d1416540f0a264eeee # Parent fbf53402134ddf240462b0c4b5c6f225e994e854 8024076: Incorrect 2 -> 4 year parsing and resolution in DateTimeFormatter Summary: Add appendValueReduced method based on a ChronoLocalDate to provide context for the value Reviewed-by: sherman Contributed-by: scolebourne@joda.org diff -r fbf53402134d -r 7891146ae141 jdk/src/share/classes/java/time/format/DateTimeFormatterBuilder.java --- a/jdk/src/share/classes/java/time/format/DateTimeFormatterBuilder.java Wed Oct 09 09:41:40 2013 -0700 +++ b/jdk/src/share/classes/java/time/format/DateTimeFormatterBuilder.java Wed Oct 09 13:34:37 2013 -0400 @@ -78,9 +78,11 @@ import java.text.ParsePosition; import java.time.DateTimeException; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.chrono.ChronoLocalDate; import java.time.chrono.Chronology; import java.time.chrono.IsoChronology; import java.time.format.DateTimeTextProvider.LocaleStore; @@ -499,51 +501,16 @@ //----------------------------------------------------------------------- /** - * Appends the reduced value of a date-time field with fixed width to the formatter. - *
- * This is typically used for formatting and parsing a two digit year. - * The {@code width} is the printed and parsed width. - * The {@code baseValue} is used during parsing to determine the valid range. - *
- * For formatting, the width is used to determine the number of characters to format. - * The rightmost characters are output to match the width, left padding with zero. - *
- * For strict parsing, the number of characters allowed by the width are parsed. - * For lenient parsing, the number of characters must be at least 1 and less than 10. - * If the number of digits parsed is equal to {@code width} and the value is positive, - * the value of the field is computed to be the first number greater than - * or equal to the {@code baseValue} with the same least significant characters, - * otherwise the value parsed is the field value. - * This allows a reduced value to be entered for values in range of the baseValue - * and width and absolute values can be entered for values outside the range. + * Appends the reduced value of a date-time field to the formatter. *
- * For example, a base value of {@code 1980} and a width of {@code 2} will have - * valid values from {@code 1980} to {@code 2079}. - * During parsing, the text {@code "12"} will result in the value {@code 2012} as that - * is the value within the range where the last two characters are "12". - * Compare with lenient parsing the text {@code "1915"} that will result in the - * value {@code 1915}. - * - * @param field the field to append, not null - * @param width the field width of the printed and parsed field, from 1 to 10 - * @param baseValue the base value of the range of valid values - * @return this, for chaining, not null - * @throws IllegalArgumentException if the width or base value is invalid - * @see #appendValueReduced(java.time.temporal.TemporalField, int, int, int) - */ - public DateTimeFormatterBuilder appendValueReduced(TemporalField field, - int width, int baseValue) { - return appendValueReduced(field, width, width, baseValue); - } - - /** - * Appends the reduced value of a date-time field with a flexible width to the formatter. - *
- * This is typically used for formatting and parsing a two digit year - * but allowing for the year value to be up to maxWidth. + * Since fields such as year vary by chronology, it is recommended to use the + * {@link #appendValueReduced(TemporalField, int, int, ChronoLocalDate)} date} + * variant of this method in most cases. This variant is suitable for + * simple fields or working with only the ISO chronology. *
* For formatting, the {@code width} and {@code maxWidth} are used to * determine the number of characters to format. + * If they are equal then the format is fixed width. * If the value of the field is within the range of the {@code baseValue} using * {@code width} characters then the reduced value is formatted otherwise the value is * truncated to fit {@code maxWidth}. @@ -562,8 +529,7 @@ * valid values from {@code 1980} to {@code 2079}. * During parsing, the text {@code "12"} will result in the value {@code 2012} as that * is the value within the range where the last two characters are "12". - * Compare with parsing the text {@code "1915"} that will result in the - * value {@code 1915}. + * By contrast, parsing the text {@code "1915"} will result in the value {@code 1915}. * * @param field the field to append, not null * @param width the field width of the printed and parsed field, from 1 to 10 @@ -575,7 +541,67 @@ public DateTimeFormatterBuilder appendValueReduced(TemporalField field, int width, int maxWidth, int baseValue) { Objects.requireNonNull(field, "field"); - ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, baseValue); + ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, baseValue, null); + appendValue(pp); + return this; + } + + /** + * Appends the reduced value of a date-time field to the formatter. + *
+ * This is typically used for formatting and parsing a two digit year. + *
+ * The base date is used to calculate the full value during parsing. + * For example, if the base date is 1950-01-01 then parsed values for + * a two digit year parse will be in the range 1950-01-01 to 2049-12-31. + * Only the year would be extracted from the date, thus a base date of + * 1950-08-25 would also parse to the range 1950-01-01 to 2049-12-31. + * This behaviour is necessary to support fields such as week-based-year + * or other calendar systems where the parsed value does not align with + * standard ISO years. + *
+ * The exact behavior is as follows. Parse the full set of fields and + * determine the effective chronology. Then convert the base date to the + * effective chronology. Then extract the specified field from the + * chronology-specific base date and use it to determine the + * {@code baseValue} used below. + *
+ * For formatting, the {@code width} and {@code maxWidth} are used to + * determine the number of characters to format. + * If they are equal then the format is fixed width. + * If the value of the field is within the range of the {@code baseValue} using + * {@code width} characters then the reduced value is formatted otherwise the value is + * truncated to fit {@code maxWidth}. + * The rightmost characters are output to match the width, left padding with zero. + *
+ * For strict parsing, the number of characters allowed by {@code width} to {@code maxWidth} are parsed. + * For lenient parsing, the number of characters must be at least 1 and less than 10. + * If the number of digits parsed is equal to {@code width} and the value is positive, + * the value of the field is computed to be the first number greater than + * or equal to the {@code baseValue} with the same least significant characters, + * otherwise the value parsed is the field value. + * This allows a reduced value to be entered for values in range of the baseValue + * and width and absolute values can be entered for values outside the range. + *
+ * For example, a base value of {@code 1980} and a width of {@code 2} will have + * valid values from {@code 1980} to {@code 2079}. + * During parsing, the text {@code "12"} will result in the value {@code 2012} as that + * is the value within the range where the last two characters are "12". + * By contrast, parsing the text {@code "1915"} will result in the value {@code 1915}. + * + * @param field the field to append, not null + * @param width the field width of the printed and parsed field, from 1 to 10 + * @param maxWidth the maximum field width of the printed field, from 1 to 10 + * @param baseDate the base date used to calculate the base value for the range + * of valid values in the parsed chronology, not null + * @return this, for chaining, not null + * @throws IllegalArgumentException if the width or base value is invalid + */ + public DateTimeFormatterBuilder appendValueReduced( + TemporalField field, int width, int maxWidth, ChronoLocalDate baseDate) { + Objects.requireNonNull(field, "field"); + Objects.requireNonNull(baseDate, "baseDate"); + ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, 0, baseDate); appendValue(pp); return this; } @@ -1682,7 +1708,7 @@ case 'u': case 'y': if (count == 2) { - appendValueReduced(field, 2, 2000); + appendValueReduced(field, 2, 2, ReducedPrinterParser.BASE_DATE); } else if (count < 4) { appendValue(field, count, 19, SignStyle.NORMAL); } else { @@ -2516,7 +2542,7 @@ if (valueLong == null) { return false; } - long value = getValue(valueLong); + long value = getValue(context, valueLong); DecimalStyle decimalStyle = context.getDecimalStyle(); String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value))); if (str.length() > maxWidth) { @@ -2560,10 +2586,11 @@ /** * Gets the value to output. * - * @param value the base value of the field, not null + * @param context the context + * @param value the value of the field, not null * @return the value */ - long getValue(long value) { + long getValue(DateTimePrintContext context, long value) { return value; } @@ -2703,7 +2730,13 @@ * Prints and parses a reduced numeric date-time field. */ static final class ReducedPrinterParser extends NumberPrinterParser { + /** + * The base date for reduced value parsing. + */ + static final LocalDate BASE_DATE = LocalDate.of(2000, 1, 1); + private final int baseValue; + private final ChronoLocalDate baseDate; /** * Constructor. @@ -2712,10 +2745,11 @@ * @param minWidth the minimum field width, from 1 to 10 * @param maxWidth the maximum field width, from 1 to 10 * @param baseValue the base value + * @param baseDate the base date */ ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, - int baseValue) { - this(field, minWidth, maxWidth, baseValue, 0); + int baseValue, ChronoLocalDate baseDate) { + this(field, minWidth, maxWidth, baseValue, baseDate, 0); if (minWidth < 1 || minWidth > 10) { throw new IllegalArgumentException("The minWidth must be from 1 to 10 inclusive but was " + minWidth); } @@ -2726,11 +2760,13 @@ throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but " + maxWidth + " < " + minWidth); } - if (field.range().isValidValue(baseValue) == false) { - throw new IllegalArgumentException("The base value must be within the range of the field"); - } - if ((((long) baseValue) + EXCEED_POINTS[maxWidth]) > Integer.MAX_VALUE) { - throw new DateTimeException("Unable to add printer-parser as the range exceeds the capacity of an int"); + if (baseDate == null) { + if (field.range().isValidValue(baseValue) == false) { + throw new IllegalArgumentException("The base value must be within the range of the field"); + } + if ((((long) baseValue) + EXCEED_POINTS[maxWidth]) > Integer.MAX_VALUE) { + throw new DateTimeException("Unable to add printer-parser as the range exceeds the capacity of an int"); + } } } @@ -2742,17 +2778,24 @@ * @param minWidth the minimum field width, from 1 to 10 * @param maxWidth the maximum field width, from 1 to 10 * @param baseValue the base value + * @param baseDate the base date * @param subsequentWidth the subsequentWidth for this instance */ private ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, - int baseValue, int subsequentWidth) { + int baseValue, ChronoLocalDate baseDate, int subsequentWidth) { super(field, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth); this.baseValue = baseValue; + this.baseDate = baseDate; } @Override - long getValue(long value) { + long getValue(DateTimePrintContext context, long value) { long absValue = Math.abs(value); + int baseValue = this.baseValue; + if (baseDate != null) { + Chronology chrono = Chronology.from(context.getTemporal()); + baseValue = chrono.date(baseDate).get(field); + } if (value >= baseValue && value < baseValue + EXCEED_POINTS[minWidth]) { // Use the reduced value if it fits in minWidth return absValue % EXCEED_POINTS[minWidth]; @@ -2763,6 +2806,12 @@ @Override int setValue(DateTimeParseContext context, long value, int errorPos, int successPos) { + int baseValue = this.baseValue; + if (baseDate != null) { + // TODO: effective chrono is inaccurate at this point + Chronology chrono = context.getEffectiveChronology(); + baseValue = chrono.date(baseDate).get(field); + } int parseLen = successPos - errorPos; if (parseLen == minWidth && value >= 0) { long range = EXCEED_POINTS[minWidth]; @@ -2773,7 +2822,7 @@ } else { value = basePart - value; } - if (basePart != 0 && value < baseValue) { + if (value < baseValue) { value += range; } } @@ -2790,7 +2839,7 @@ if (subsequentWidth == -1) { return this; } - return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, -1); + return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, baseDate, -1); } /** @@ -2801,13 +2850,13 @@ */ @Override ReducedPrinterParser withSubsequentWidth(int subsequentWidth) { - return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, + return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, baseDate, this.subsequentWidth + subsequentWidth); } @Override public String toString() { - return "ReducedValue(" + field + "," + minWidth + "," + maxWidth + "," + baseValue + ")"; + return "ReducedValue(" + field + "," + minWidth + "," + maxWidth + "," + (baseDate != null ? baseDate : baseValue) + ")"; } } @@ -4351,7 +4400,7 @@ case 'Y': field = weekDef.weekBasedYear(); if (count == 2) { - return new ReducedPrinterParser(field, 2, 2, 2000, 0); + return new ReducedPrinterParser(field, 2, 2, 0, ReducedPrinterParser.BASE_DATE, 0); } else { return new NumberPrinterParser(field, count, 19, (count < 4) ? SignStyle.NORMAL : SignStyle.EXCEEDS_PAD, -1); @@ -4380,7 +4429,7 @@ if (count == 1) { sb.append("WeekBasedYear"); } else if (count == 2) { - sb.append("ReducedValue(WeekBasedYear,2,2000)"); + sb.append("ReducedValue(WeekBasedYear,2,2,2000-01-01)"); } else { sb.append("WeekBasedYear,").append(count).append(",") .append(19).append(",") diff -r fbf53402134d -r 7891146ae141 jdk/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java --- a/jdk/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java Wed Oct 09 09:41:40 2013 -0700 +++ b/jdk/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java Wed Oct 09 13:34:37 2013 -0400 @@ -190,8 +190,69 @@ //----------------------------------------------------------------------- @Test(expectedExceptions=NullPointerException.class) - public void test_appendValueReduced_null() throws Exception { - builder.appendValueReduced(null, 2, 2000); + public void test_appendValueReduced_int_nullField() throws Exception { + builder.appendValueReduced(null, 2, 2, 2000); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_int_minWidthTooSmall() throws Exception { + builder.appendValueReduced(YEAR, 0, 2, 2000); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_int_minWidthTooBig() throws Exception { + builder.appendValueReduced(YEAR, 11, 2, 2000); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_int_maxWidthTooSmall() throws Exception { + builder.appendValueReduced(YEAR, 2, 0, 2000); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_int_maxWidthTooBig() throws Exception { + builder.appendValueReduced(YEAR, 2, 11, 2000); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_int_maxWidthLessThanMin() throws Exception { + builder.appendValueReduced(YEAR, 2, 1, 2000); + } + + //----------------------------------------------------------------------- + @Test(expectedExceptions=NullPointerException.class) + public void test_appendValueReduced_date_nullField() throws Exception { + builder.appendValueReduced(null, 2, 2, LocalDate.of(2000, 1, 1)); + } + + @Test(expectedExceptions=NullPointerException.class) + public void test_appendValueReduced_date_nullDate() throws Exception { + builder.appendValueReduced(YEAR, 2, 2, null); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_date_minWidthTooSmall() throws Exception { + builder.appendValueReduced(YEAR, 0, 2, LocalDate.of(2000, 1, 1)); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_date_minWidthTooBig() throws Exception { + builder.appendValueReduced(YEAR, 11, 2, LocalDate.of(2000, 1, 1)); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_date_maxWidthTooSmall() throws Exception { + builder.appendValueReduced(YEAR, 2, 0, LocalDate.of(2000, 1, 1)); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_date_maxWidthTooBig() throws Exception { + builder.appendValueReduced(YEAR, 2, 11, LocalDate.of(2000, 1, 1)); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void test_appendValueReduced_date_maxWidthLessThanMin() throws Exception { + builder.appendValueReduced(YEAR, 2, 1, LocalDate.of(2000, 1, 1)); } //----------------------------------------------------------------------- diff -r fbf53402134d -r 7891146ae141 jdk/test/java/time/test/java/time/format/TestDateTimeFormatterBuilder.java --- a/jdk/test/java/time/test/java/time/format/TestDateTimeFormatterBuilder.java Wed Oct 09 09:41:40 2013 -0700 +++ b/jdk/test/java/time/test/java/time/format/TestDateTimeFormatterBuilder.java Wed Oct 09 13:34:37 2013 -0400 @@ -267,12 +267,12 @@ //----------------------------------------------------------------------- @Test(expectedExceptions=NullPointerException.class) public void test_appendValueReduced_null() throws Exception { - builder.appendValueReduced(null, 2, 2000); + builder.appendValueReduced(null, 2, 2, 2000); } @Test public void test_appendValueReduced() throws Exception { - builder.appendValueReduced(YEAR, 2, 2000); + builder.appendValueReduced(YEAR, 2, 2, 2000); DateTimeFormatter f = builder.toFormatter(); assertEquals(f.toString(), "ReducedValue(Year,2,2,2000)"); TemporalAccessor parsed = f.parseUnresolved("12", new ParsePosition(0)); @@ -281,7 +281,7 @@ @Test public void test_appendValueReduced_subsequent_parse() throws Exception { - builder.appendValue(MONTH_OF_YEAR, 1, 2, SignStyle.NORMAL).appendValueReduced(YEAR, 2, 2000); + builder.appendValue(MONTH_OF_YEAR, 1, 2, SignStyle.NORMAL).appendValueReduced(YEAR, 2, 2, 2000); DateTimeFormatter f = builder.toFormatter(); assertEquals(f.toString(), "Value(MonthOfYear,1,2,NORMAL)ReducedValue(Year,2,2,2000)"); ParsePosition ppos = new ParsePosition(0); @@ -654,19 +654,19 @@ {"GGGGG", "Text(Era,NARROW)"}, {"u", "Value(Year)"}, - {"uu", "ReducedValue(Year,2,2,2000)"}, + {"uu", "ReducedValue(Year,2,2,2000-01-01)"}, {"uuu", "Value(Year,3,19,NORMAL)"}, {"uuuu", "Value(Year,4,19,EXCEEDS_PAD)"}, {"uuuuu", "Value(Year,5,19,EXCEEDS_PAD)"}, {"y", "Value(YearOfEra)"}, - {"yy", "ReducedValue(YearOfEra,2,2,2000)"}, + {"yy", "ReducedValue(YearOfEra,2,2,2000-01-01)"}, {"yyy", "Value(YearOfEra,3,19,NORMAL)"}, {"yyyy", "Value(YearOfEra,4,19,EXCEEDS_PAD)"}, {"yyyyy", "Value(YearOfEra,5,19,EXCEEDS_PAD)"}, {"Y", "Localized(WeekBasedYear)"}, - {"YY", "Localized(ReducedValue(WeekBasedYear,2,2000))"}, + {"YY", "Localized(ReducedValue(WeekBasedYear,2,2,2000-01-01))"}, {"YYY", "Localized(WeekBasedYear,3,19,NORMAL)"}, {"YYYY", "Localized(WeekBasedYear,4,19,EXCEEDS_PAD)"}, {"YYYYY", "Localized(WeekBasedYear,5,19,EXCEEDS_PAD)"}, diff -r fbf53402134d -r 7891146ae141 jdk/test/java/time/test/java/time/format/TestReducedParser.java --- a/jdk/test/java/time/test/java/time/format/TestReducedParser.java Wed Oct 09 09:41:40 2013 -0700 +++ b/jdk/test/java/time/test/java/time/format/TestReducedParser.java Wed Oct 09 13:34:37 2013 -0400 @@ -64,11 +64,20 @@ import static java.time.temporal.ChronoField.MONTH_OF_YEAR; import static java.time.temporal.ChronoField.YEAR; import static java.time.temporal.ChronoField.YEAR_OF_ERA; +import static java.time.temporal.ChronoUnit.YEARS; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertNotNull; import java.text.ParsePosition; +import java.time.LocalDate; +import java.time.chrono.Chronology; +import java.time.chrono.ChronoLocalDate; +import java.time.chrono.HijrahChronology; +import java.time.chrono.IsoChronology; +import java.time.chrono.JapaneseChronology; +import java.time.chrono.MinguoChronology; +import java.time.chrono.ThaiBuddhistChronology; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.TemporalAccessor; @@ -86,13 +95,17 @@ private static final boolean LENIENT = false; private DateTimeFormatter getFormatter0(TemporalField field, int width, int baseValue) { - return builder.appendValueReduced(field, width, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); + return builder.appendValueReduced(field, width, width, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); } private DateTimeFormatter getFormatter0(TemporalField field, int minWidth, int maxWidth, int baseValue) { return builder.appendValueReduced(field, minWidth, maxWidth, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); } + private DateTimeFormatter getFormatterBaseDate(TemporalField field, int minWidth, int maxWidth, int baseValue) { + return builder.appendValueReduced(field, minWidth, maxWidth, LocalDate.of(baseValue, 1, 1)).toFormatter(locale).withDecimalStyle(decimalStyle); + } + //----------------------------------------------------------------------- @DataProvider(name="error") Object[][] data_error() { @@ -243,6 +256,10 @@ // Negative baseValue {YEAR, 2, 4, -2005, "123", 0, strict(3, 123), lenient(3, 123)}, + + // Basics + {YEAR, 2, 4, 2010, "10", 0, strict(2, 2010), lenient(2, 2010)}, + {YEAR, 2, 4, 2010, "09", 0, strict(2, 2109), lenient(2, 2109)}, }; } @@ -264,6 +281,21 @@ } } + @Test(dataProvider="ParseLenientSensitive") + public void test_parseStrict_baseDate(TemporalField field, int minWidth, int maxWidth, int baseValue, String input, int pos, + Pair strict, Pair lenient) { + ParsePosition ppos = new ParsePosition(pos); + setStrict(true); + TemporalAccessor parsed = getFormatterBaseDate(field, minWidth, maxWidth, baseValue).parseUnresolved(input, ppos); + if (ppos.getErrorIndex() != -1) { + assertEquals(ppos.getErrorIndex(), strict.parseLen, "error case parse position"); + assertEquals(parsed, strict.parseVal, "unexpected parse result"); + } else { + assertEquals(ppos.getIndex(), strict.parseLen, "parse position"); + assertParsed(parsed, YEAR, strict.parseVal != null ? (long) strict.parseVal : null); + } + } + //----------------------------------------------------------------------- // Parsing tests for lenient mode //----------------------------------------------------------------------- @@ -282,6 +314,21 @@ } } + @Test(dataProvider="ParseLenientSensitive") + public void test_parseLenient_baseDate(TemporalField field, int minWidth, int maxWidth, int baseValue, String input, int pos, + Pair strict, Pair lenient) { + ParsePosition ppos = new ParsePosition(pos); + setStrict(false); + TemporalAccessor parsed = getFormatterBaseDate(field, minWidth, maxWidth, baseValue).parseUnresolved(input, ppos); + if (ppos.getErrorIndex() != -1) { + assertEquals(ppos.getErrorIndex(), lenient.parseLen, "error case parse position"); + assertEquals(parsed, lenient.parseVal, "unexpected parse result"); + } else { + assertEquals(ppos.getIndex(), lenient.parseLen, "parse position"); + assertParsed(parsed, YEAR, lenient.parseVal != null ? (long) lenient.parseVal : null); + } + } + private void assertParsed(TemporalAccessor parsed, TemporalField field, Long value) { if (value == null) { assertEquals(parsed, null, "Parsed Value"); @@ -335,6 +382,68 @@ } //----------------------------------------------------------------------- + // Cases and values in reduced value parsing mode + //----------------------------------------------------------------------- + @DataProvider(name="ReducedWithChrono") + Object[][] provider_reducedWithChrono() { + LocalDate baseYear = LocalDate.of(2000, 1, 1); + return new Object[][] { + {IsoChronology.INSTANCE.date(baseYear)}, + {IsoChronology.INSTANCE.date(baseYear).plus(1, YEARS)}, + {IsoChronology.INSTANCE.date(baseYear).plus(99, YEARS)}, + {HijrahChronology.INSTANCE.date(baseYear)}, + {HijrahChronology.INSTANCE.date(baseYear).plus(1, YEARS)}, + {HijrahChronology.INSTANCE.date(baseYear).plus(99, YEARS)}, + {JapaneseChronology.INSTANCE.date(baseYear)}, + {JapaneseChronology.INSTANCE.date(baseYear).plus(1, YEARS)}, + {JapaneseChronology.INSTANCE.date(baseYear).plus(99, YEARS)}, + {MinguoChronology.INSTANCE.date(baseYear)}, + {MinguoChronology.INSTANCE.date(baseYear).plus(1, YEARS)}, + {MinguoChronology.INSTANCE.date(baseYear).plus(99, YEARS)}, + {ThaiBuddhistChronology.INSTANCE.date(baseYear)}, + {ThaiBuddhistChronology.INSTANCE.date(baseYear).plus(1, YEARS)}, + {ThaiBuddhistChronology.INSTANCE.date(baseYear).plus(99, YEARS)}, + }; + } + + @Test(dataProvider="ReducedWithChrono") + public void test_reducedWithChronoYear(ChronoLocalDate date) { + Chronology chrono = date.getChronology(); + DateTimeFormatter df + = new DateTimeFormatterBuilder().appendValueReduced(YEAR, 2, 2, LocalDate.of(2000, 1, 1)) + .toFormatter() + .withChronology(chrono); + int expected = date.get(YEAR); + String input = df.format(date); + + ParsePosition pos = new ParsePosition(0); + TemporalAccessor parsed = df.parseUnresolved(input, pos); + int actual = parsed.get(YEAR); + assertEquals(actual, expected, + String.format("Wrong date parsed, chrono: %s, input: %s", + chrono, input)); + + } + @Test(dataProvider="ReducedWithChrono") + public void test_reducedWithChronoYearOfEra(ChronoLocalDate date) { + Chronology chrono = date.getChronology(); + DateTimeFormatter df + = new DateTimeFormatterBuilder().appendValueReduced(YEAR_OF_ERA, 2, 2, LocalDate.of(2000, 1, 1)) + .toFormatter() + .withChronology(chrono); + int expected = date.get(YEAR_OF_ERA); + String input = df.format(date); + + ParsePosition pos = new ParsePosition(0); + TemporalAccessor parsed = df.parseUnresolved(input, pos); + int actual = parsed.get(YEAR_OF_ERA); + assertEquals(actual, expected, + String.format("Wrong date parsed, chrono: %s, input: %s", + chrono, input)); + + } + + //----------------------------------------------------------------------- // Class to structure the test data //----------------------------------------------------------------------- diff -r fbf53402134d -r 7891146ae141 jdk/test/java/time/test/java/time/format/TestReducedPrinter.java --- a/jdk/test/java/time/test/java/time/format/TestReducedPrinter.java Wed Oct 09 09:41:40 2013 -0700 +++ b/jdk/test/java/time/test/java/time/format/TestReducedPrinter.java Wed Oct 09 13:34:37 2013 -0400 @@ -59,19 +59,15 @@ */ package test.java.time.format; -import java.text.ParsePosition; import static java.time.temporal.ChronoField.YEAR; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; import java.time.DateTimeException; import java.time.LocalDate; +import java.time.chrono.MinguoDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import static java.time.temporal.ChronoField.DAY_OF_MONTH; -import static java.time.temporal.ChronoField.MONTH_OF_YEAR; -import static java.time.temporal.ChronoField.YEAR_OF_ERA; -import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; import org.testng.annotations.DataProvider; @@ -85,13 +81,17 @@ public class TestReducedPrinter extends AbstractTestPrinterParser { private DateTimeFormatter getFormatter0(TemporalField field, int width, int baseValue) { - return builder.appendValueReduced(field, width, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); + return builder.appendValueReduced(field, width, width, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); } private DateTimeFormatter getFormatter0(TemporalField field, int minWidth, int maxWidth, int baseValue) { return builder.appendValueReduced(field, minWidth, maxWidth, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); } + private DateTimeFormatter getFormatterBaseDate(TemporalField field, int minWidth, int maxWidth, int baseValue) { + return builder.appendValueReduced(field, minWidth, maxWidth, LocalDate.of(baseValue, 1, 1)).toFormatter(locale).withDecimalStyle(decimalStyle); + } + //----------------------------------------------------------------------- @Test(expectedExceptions=DateTimeException.class) public void test_print_emptyCalendrical() throws Exception { @@ -192,6 +192,58 @@ } } + @Test(dataProvider="Pivot") + public void test_pivot_baseDate(int minWidth, int maxWidth, int baseValue, int value, String result) throws Exception { + try { + getFormatterBaseDate(YEAR, minWidth, maxWidth, baseValue).formatTo(new MockFieldValue(YEAR, value), buf); + if (result == null) { + fail("Expected exception"); + } + assertEquals(buf.toString(), result); + } catch (DateTimeException ex) { + if (result == null || value < 0) { + assertEquals(ex.getMessage().contains(YEAR.toString()), true); + } else { + throw ex; + } + } + } + + //----------------------------------------------------------------------- + public void test_minguoChrono_fixedWidth() throws Exception { + // ISO 2021 is Minguo 110 + DateTimeFormatter f = getFormatterBaseDate(YEAR, 2, 2, 2021); + MinguoDate date = MinguoDate.of(109, 6, 30); + assertEquals(f.format(date), "09"); + date = MinguoDate.of(110, 6, 30); + assertEquals(f.format(date), "10"); + date = MinguoDate.of(199, 6, 30); + assertEquals(f.format(date), "99"); + date = MinguoDate.of(200, 6, 30); + assertEquals(f.format(date), "00"); + date = MinguoDate.of(209, 6, 30); + assertEquals(f.format(date), "09"); + date = MinguoDate.of(210, 6, 30); + assertEquals(f.format(date), "10"); + } + + public void test_minguoChrono_extendedWidth() throws Exception { + // ISO 2021 is Minguo 110 + DateTimeFormatter f = getFormatterBaseDate(YEAR, 2, 4, 2021); + MinguoDate date = MinguoDate.of(109, 6, 30); + assertEquals(f.format(date), "109"); + date = MinguoDate.of(110, 6, 30); + assertEquals(f.format(date), "10"); + date = MinguoDate.of(199, 6, 30); + assertEquals(f.format(date), "99"); + date = MinguoDate.of(200, 6, 30); + assertEquals(f.format(date), "00"); + date = MinguoDate.of(209, 6, 30); + assertEquals(f.format(date), "09"); + date = MinguoDate.of(210, 6, 30); + assertEquals(f.format(date), "210"); + } + //----------------------------------------------------------------------- public void test_toString() throws Exception { assertEquals(getFormatter0(YEAR, 2, 2, 2005).toString(), "ReducedValue(Year,2,2,2005)");