changeset 8344:d42fe440bda8

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
author rriggs
date Wed, 09 Oct 2013 13:34:37 -0400
parents c070001c4f60
children b86e6700266e
files src/share/classes/java/time/format/DateTimeFormatterBuilder.java test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java test/java/time/test/java/time/format/TestDateTimeFormatterBuilder.java test/java/time/test/java/time/format/TestReducedParser.java test/java/time/test/java/time/format/TestReducedPrinter.java
diffstat 5 files changed, 349 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/java/time/format/DateTimeFormatterBuilder.java	Wed Oct 09 09:41:40 2013 -0700
+++ b/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.
+     * Appends the reduced value of a date-time field to the formatter.
      * <p>
-     * 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.
-     * <p>
-     * 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.
-     * <p>
-     * 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.
-     * <p>
-     * 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.
-     * <p>
-     * 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.
      * <p>
      * 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.
+     * <p>
+     * This is typically used for formatting and parsing a two digit year.
+     * <p>
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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(",")
--- a/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java	Wed Oct 09 09:41:40 2013 -0700
+++ b/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));
     }
 
     //-----------------------------------------------------------------------
--- a/test/java/time/test/java/time/format/TestDateTimeFormatterBuilder.java	Wed Oct 09 09:41:40 2013 -0700
+++ b/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)"},
--- a/test/java/time/test/java/time/format/TestReducedParser.java	Wed Oct 09 09:41:40 2013 -0700
+++ b/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
     //-----------------------------------------------------------------------
 
--- a/test/java/time/test/java/time/format/TestReducedPrinter.java	Wed Oct 09 09:41:40 2013 -0700
+++ b/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)");