提交 1d57dc51 编写于 作者: R rriggs

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
上级 10b84a64
...@@ -78,9 +78,11 @@ import java.math.RoundingMode; ...@@ -78,9 +78,11 @@ import java.math.RoundingMode;
import java.text.ParsePosition; import java.text.ParsePosition;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.Chronology; import java.time.chrono.Chronology;
import java.time.chrono.IsoChronology; import java.time.chrono.IsoChronology;
import java.time.format.DateTimeTextProvider.LocaleStore; import java.time.format.DateTimeTextProvider.LocaleStore;
...@@ -499,16 +501,22 @@ public final class DateTimeFormatterBuilder { ...@@ -499,16 +501,22 @@ public final class DateTimeFormatterBuilder {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
/** /**
* 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> * <p>
* This is typically used for formatting and parsing a two digit year. * Since fields such as year vary by chronology, it is recommended to use the
* The {@code width} is the printed and parsed width. * {@link #appendValueReduced(TemporalField, int, int, ChronoLocalDate)} date}
* The {@code baseValue} is used during parsing to determine the valid range. * variant of this method in most cases. This variant is suitable for
* simple fields or working with only the ISO chronology.
* <p> * <p>
* For formatting, the width is used to determine the number of characters to format. * 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. * The rightmost characters are output to match the width, left padding with zero.
* <p> * <p>
* For strict parsing, the number of characters allowed by the width are parsed. * 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. * 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, * 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 * the value of the field is computed to be the first number greater than
...@@ -521,29 +529,46 @@ public final class DateTimeFormatterBuilder { ...@@ -521,29 +529,46 @@ public final class DateTimeFormatterBuilder {
* valid values from {@code 1980} to {@code 2079}. * valid values from {@code 1980} to {@code 2079}.
* During parsing, the text {@code "12"} will result in the value {@code 2012} as that * 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". * 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 * By contrast, parsing the text {@code "1915"} will result in the value {@code 1915}.
* value {@code 1915}.
* *
* @param field the field to append, not null * @param field the field to append, not null
* @param width the field width of the printed and parsed field, from 1 to 10 * @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 baseValue the base value of the range of valid values * @param baseValue the base value of the range of valid values
* @return this, for chaining, not null * @return this, for chaining, not null
* @throws IllegalArgumentException if the width or base value is invalid * @throws IllegalArgumentException if the width or base value is invalid
* @see #appendValueReduced(java.time.temporal.TemporalField, int, int, int)
*/ */
public DateTimeFormatterBuilder appendValueReduced(TemporalField field, public DateTimeFormatterBuilder appendValueReduced(TemporalField field,
int width, int baseValue) { int width, int maxWidth, int baseValue) {
return appendValueReduced(field, width, width, baseValue); Objects.requireNonNull(field, "field");
ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, baseValue, null);
appendValue(pp);
return this;
} }
/** /**
* Appends the reduced value of a date-time field with a flexible width to the formatter. * Appends the reduced value of a date-time field to the formatter.
* <p> * <p>
* This is typically used for formatting and parsing a two digit year * This is typically used for formatting and parsing a two digit year.
* but allowing for the year value to be up to maxWidth. * <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> * <p>
* For formatting, the {@code width} and {@code maxWidth} are used to * For formatting, the {@code width} and {@code maxWidth} are used to
* determine the number of characters to format. * 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 * 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 * {@code width} characters then the reduced value is formatted otherwise the value is
* truncated to fit {@code maxWidth}. * truncated to fit {@code maxWidth}.
...@@ -562,20 +587,21 @@ public final class DateTimeFormatterBuilder { ...@@ -562,20 +587,21 @@ public final class DateTimeFormatterBuilder {
* valid values from {@code 1980} to {@code 2079}. * valid values from {@code 1980} to {@code 2079}.
* During parsing, the text {@code "12"} will result in the value {@code 2012} as that * 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". * 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 * By contrast, parsing the text {@code "1915"} will result in the value {@code 1915}.
* value {@code 1915}.
* *
* @param field the field to append, not null * @param field the field to append, not null
* @param width the field width of the printed and parsed field, from 1 to 10 * @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 maxWidth the maximum field width of the printed field, from 1 to 10
* @param baseValue the base value of the range of valid values * @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 * @return this, for chaining, not null
* @throws IllegalArgumentException if the width or base value is invalid * @throws IllegalArgumentException if the width or base value is invalid
*/ */
public DateTimeFormatterBuilder appendValueReduced(TemporalField field, public DateTimeFormatterBuilder appendValueReduced(
int width, int maxWidth, int baseValue) { TemporalField field, int width, int maxWidth, ChronoLocalDate baseDate) {
Objects.requireNonNull(field, "field"); Objects.requireNonNull(field, "field");
ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, baseValue); Objects.requireNonNull(baseDate, "baseDate");
ReducedPrinterParser pp = new ReducedPrinterParser(field, width, maxWidth, 0, baseDate);
appendValue(pp); appendValue(pp);
return this; return this;
} }
...@@ -1682,7 +1708,7 @@ public final class DateTimeFormatterBuilder { ...@@ -1682,7 +1708,7 @@ public final class DateTimeFormatterBuilder {
case 'u': case 'u':
case 'y': case 'y':
if (count == 2) { if (count == 2) {
appendValueReduced(field, 2, 2000); appendValueReduced(field, 2, 2, ReducedPrinterParser.BASE_DATE);
} else if (count < 4) { } else if (count < 4) {
appendValue(field, count, 19, SignStyle.NORMAL); appendValue(field, count, 19, SignStyle.NORMAL);
} else { } else {
...@@ -2516,7 +2542,7 @@ public final class DateTimeFormatterBuilder { ...@@ -2516,7 +2542,7 @@ public final class DateTimeFormatterBuilder {
if (valueLong == null) { if (valueLong == null) {
return false; return false;
} }
long value = getValue(valueLong); long value = getValue(context, valueLong);
DecimalStyle decimalStyle = context.getDecimalStyle(); DecimalStyle decimalStyle = context.getDecimalStyle();
String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value))); String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value)));
if (str.length() > maxWidth) { if (str.length() > maxWidth) {
...@@ -2560,10 +2586,11 @@ public final class DateTimeFormatterBuilder { ...@@ -2560,10 +2586,11 @@ public final class DateTimeFormatterBuilder {
/** /**
* Gets the value to output. * 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 * @return the value
*/ */
long getValue(long value) { long getValue(DateTimePrintContext context, long value) {
return value; return value;
} }
...@@ -2703,7 +2730,13 @@ public final class DateTimeFormatterBuilder { ...@@ -2703,7 +2730,13 @@ public final class DateTimeFormatterBuilder {
* Prints and parses a reduced numeric date-time field. * Prints and parses a reduced numeric date-time field.
*/ */
static final class ReducedPrinterParser extends NumberPrinterParser { 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 int baseValue;
private final ChronoLocalDate baseDate;
/** /**
* Constructor. * Constructor.
...@@ -2712,10 +2745,11 @@ public final class DateTimeFormatterBuilder { ...@@ -2712,10 +2745,11 @@ public final class DateTimeFormatterBuilder {
* @param minWidth the minimum field width, from 1 to 10 * @param minWidth the minimum field width, from 1 to 10
* @param maxWidth the maximum field width, from 1 to 10 * @param maxWidth the maximum field width, from 1 to 10
* @param baseValue the base value * @param baseValue the base value
* @param baseDate the base date
*/ */
ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth,
int baseValue) { int baseValue, ChronoLocalDate baseDate) {
this(field, minWidth, maxWidth, baseValue, 0); this(field, minWidth, maxWidth, baseValue, baseDate, 0);
if (minWidth < 1 || minWidth > 10) { if (minWidth < 1 || minWidth > 10) {
throw new IllegalArgumentException("The minWidth must be from 1 to 10 inclusive but was " + minWidth); throw new IllegalArgumentException("The minWidth must be from 1 to 10 inclusive but was " + minWidth);
} }
...@@ -2726,6 +2760,7 @@ public final class DateTimeFormatterBuilder { ...@@ -2726,6 +2760,7 @@ public final class DateTimeFormatterBuilder {
throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but " + throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but " +
maxWidth + " < " + minWidth); maxWidth + " < " + minWidth);
} }
if (baseDate == null) {
if (field.range().isValidValue(baseValue) == false) { if (field.range().isValidValue(baseValue) == false) {
throw new IllegalArgumentException("The base value must be within the range of the field"); throw new IllegalArgumentException("The base value must be within the range of the field");
} }
...@@ -2733,6 +2768,7 @@ public final class DateTimeFormatterBuilder { ...@@ -2733,6 +2768,7 @@ public final class DateTimeFormatterBuilder {
throw new DateTimeException("Unable to add printer-parser as the range exceeds the capacity of an int"); throw new DateTimeException("Unable to add printer-parser as the range exceeds the capacity of an int");
} }
} }
}
/** /**
* Constructor. * Constructor.
...@@ -2742,17 +2778,24 @@ public final class DateTimeFormatterBuilder { ...@@ -2742,17 +2778,24 @@ public final class DateTimeFormatterBuilder {
* @param minWidth the minimum field width, from 1 to 10 * @param minWidth the minimum field width, from 1 to 10
* @param maxWidth the maximum field width, from 1 to 10 * @param maxWidth the maximum field width, from 1 to 10
* @param baseValue the base value * @param baseValue the base value
* @param baseDate the base date
* @param subsequentWidth the subsequentWidth for this instance * @param subsequentWidth the subsequentWidth for this instance
*/ */
private ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth, 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); super(field, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth);
this.baseValue = baseValue; this.baseValue = baseValue;
this.baseDate = baseDate;
} }
@Override @Override
long getValue(long value) { long getValue(DateTimePrintContext context, long value) {
long absValue = Math.abs(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]) { if (value >= baseValue && value < baseValue + EXCEED_POINTS[minWidth]) {
// Use the reduced value if it fits in minWidth // Use the reduced value if it fits in minWidth
return absValue % EXCEED_POINTS[minWidth]; return absValue % EXCEED_POINTS[minWidth];
...@@ -2763,6 +2806,12 @@ public final class DateTimeFormatterBuilder { ...@@ -2763,6 +2806,12 @@ public final class DateTimeFormatterBuilder {
@Override @Override
int setValue(DateTimeParseContext context, long value, int errorPos, int successPos) { 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; int parseLen = successPos - errorPos;
if (parseLen == minWidth && value >= 0) { if (parseLen == minWidth && value >= 0) {
long range = EXCEED_POINTS[minWidth]; long range = EXCEED_POINTS[minWidth];
...@@ -2773,7 +2822,7 @@ public final class DateTimeFormatterBuilder { ...@@ -2773,7 +2822,7 @@ public final class DateTimeFormatterBuilder {
} else { } else {
value = basePart - value; value = basePart - value;
} }
if (basePart != 0 && value < baseValue) { if (value < baseValue) {
value += range; value += range;
} }
} }
...@@ -2790,7 +2839,7 @@ public final class DateTimeFormatterBuilder { ...@@ -2790,7 +2839,7 @@ public final class DateTimeFormatterBuilder {
if (subsequentWidth == -1) { if (subsequentWidth == -1) {
return this; return this;
} }
return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, -1); return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, baseDate, -1);
} }
/** /**
...@@ -2801,13 +2850,13 @@ public final class DateTimeFormatterBuilder { ...@@ -2801,13 +2850,13 @@ public final class DateTimeFormatterBuilder {
*/ */
@Override @Override
ReducedPrinterParser withSubsequentWidth(int subsequentWidth) { ReducedPrinterParser withSubsequentWidth(int subsequentWidth) {
return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, return new ReducedPrinterParser(field, minWidth, maxWidth, baseValue, baseDate,
this.subsequentWidth + subsequentWidth); this.subsequentWidth + subsequentWidth);
} }
@Override @Override
public String toString() { public String toString() {
return "ReducedValue(" + field + "," + minWidth + "," + maxWidth + "," + baseValue + ")"; return "ReducedValue(" + field + "," + minWidth + "," + maxWidth + "," + (baseDate != null ? baseDate : baseValue) + ")";
} }
} }
...@@ -4351,7 +4400,7 @@ public final class DateTimeFormatterBuilder { ...@@ -4351,7 +4400,7 @@ public final class DateTimeFormatterBuilder {
case 'Y': case 'Y':
field = weekDef.weekBasedYear(); field = weekDef.weekBasedYear();
if (count == 2) { if (count == 2) {
return new ReducedPrinterParser(field, 2, 2, 2000, 0); return new ReducedPrinterParser(field, 2, 2, 0, ReducedPrinterParser.BASE_DATE, 0);
} else { } else {
return new NumberPrinterParser(field, count, 19, return new NumberPrinterParser(field, count, 19,
(count < 4) ? SignStyle.NORMAL : SignStyle.EXCEEDS_PAD, -1); (count < 4) ? SignStyle.NORMAL : SignStyle.EXCEEDS_PAD, -1);
...@@ -4380,7 +4429,7 @@ public final class DateTimeFormatterBuilder { ...@@ -4380,7 +4429,7 @@ public final class DateTimeFormatterBuilder {
if (count == 1) { if (count == 1) {
sb.append("WeekBasedYear"); sb.append("WeekBasedYear");
} else if (count == 2) { } else if (count == 2) {
sb.append("ReducedValue(WeekBasedYear,2,2000)"); sb.append("ReducedValue(WeekBasedYear,2,2,2000-01-01)");
} else { } else {
sb.append("WeekBasedYear,").append(count).append(",") sb.append("WeekBasedYear,").append(count).append(",")
.append(19).append(",") .append(19).append(",")
......
...@@ -190,8 +190,69 @@ public class TCKDateTimeFormatterBuilder { ...@@ -190,8 +190,69 @@ public class TCKDateTimeFormatterBuilder {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@Test(expectedExceptions=NullPointerException.class) @Test(expectedExceptions=NullPointerException.class)
public void test_appendValueReduced_null() throws Exception { public void test_appendValueReduced_int_nullField() throws Exception {
builder.appendValueReduced(null, 2, 2000); 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));
} }
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
......
...@@ -267,12 +267,12 @@ public class TestDateTimeFormatterBuilder { ...@@ -267,12 +267,12 @@ public class TestDateTimeFormatterBuilder {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@Test(expectedExceptions=NullPointerException.class) @Test(expectedExceptions=NullPointerException.class)
public void test_appendValueReduced_null() throws Exception { public void test_appendValueReduced_null() throws Exception {
builder.appendValueReduced(null, 2, 2000); builder.appendValueReduced(null, 2, 2, 2000);
} }
@Test @Test
public void test_appendValueReduced() throws Exception { public void test_appendValueReduced() throws Exception {
builder.appendValueReduced(YEAR, 2, 2000); builder.appendValueReduced(YEAR, 2, 2, 2000);
DateTimeFormatter f = builder.toFormatter(); DateTimeFormatter f = builder.toFormatter();
assertEquals(f.toString(), "ReducedValue(Year,2,2,2000)"); assertEquals(f.toString(), "ReducedValue(Year,2,2,2000)");
TemporalAccessor parsed = f.parseUnresolved("12", new ParsePosition(0)); TemporalAccessor parsed = f.parseUnresolved("12", new ParsePosition(0));
...@@ -281,7 +281,7 @@ public class TestDateTimeFormatterBuilder { ...@@ -281,7 +281,7 @@ public class TestDateTimeFormatterBuilder {
@Test @Test
public void test_appendValueReduced_subsequent_parse() throws Exception { 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(); DateTimeFormatter f = builder.toFormatter();
assertEquals(f.toString(), "Value(MonthOfYear,1,2,NORMAL)ReducedValue(Year,2,2,2000)"); assertEquals(f.toString(), "Value(MonthOfYear,1,2,NORMAL)ReducedValue(Year,2,2,2000)");
ParsePosition ppos = new ParsePosition(0); ParsePosition ppos = new ParsePosition(0);
...@@ -654,19 +654,19 @@ public class TestDateTimeFormatterBuilder { ...@@ -654,19 +654,19 @@ public class TestDateTimeFormatterBuilder {
{"GGGGG", "Text(Era,NARROW)"}, {"GGGGG", "Text(Era,NARROW)"},
{"u", "Value(Year)"}, {"u", "Value(Year)"},
{"uu", "ReducedValue(Year,2,2,2000)"}, {"uu", "ReducedValue(Year,2,2,2000-01-01)"},
{"uuu", "Value(Year,3,19,NORMAL)"}, {"uuu", "Value(Year,3,19,NORMAL)"},
{"uuuu", "Value(Year,4,19,EXCEEDS_PAD)"}, {"uuuu", "Value(Year,4,19,EXCEEDS_PAD)"},
{"uuuuu", "Value(Year,5,19,EXCEEDS_PAD)"}, {"uuuuu", "Value(Year,5,19,EXCEEDS_PAD)"},
{"y", "Value(YearOfEra)"}, {"y", "Value(YearOfEra)"},
{"yy", "ReducedValue(YearOfEra,2,2,2000)"}, {"yy", "ReducedValue(YearOfEra,2,2,2000-01-01)"},
{"yyy", "Value(YearOfEra,3,19,NORMAL)"}, {"yyy", "Value(YearOfEra,3,19,NORMAL)"},
{"yyyy", "Value(YearOfEra,4,19,EXCEEDS_PAD)"}, {"yyyy", "Value(YearOfEra,4,19,EXCEEDS_PAD)"},
{"yyyyy", "Value(YearOfEra,5,19,EXCEEDS_PAD)"}, {"yyyyy", "Value(YearOfEra,5,19,EXCEEDS_PAD)"},
{"Y", "Localized(WeekBasedYear)"}, {"Y", "Localized(WeekBasedYear)"},
{"YY", "Localized(ReducedValue(WeekBasedYear,2,2000))"}, {"YY", "Localized(ReducedValue(WeekBasedYear,2,2,2000-01-01))"},
{"YYY", "Localized(WeekBasedYear,3,19,NORMAL)"}, {"YYY", "Localized(WeekBasedYear,3,19,NORMAL)"},
{"YYYY", "Localized(WeekBasedYear,4,19,EXCEEDS_PAD)"}, {"YYYY", "Localized(WeekBasedYear,4,19,EXCEEDS_PAD)"},
{"YYYYY", "Localized(WeekBasedYear,5,19,EXCEEDS_PAD)"}, {"YYYYY", "Localized(WeekBasedYear,5,19,EXCEEDS_PAD)"},
......
...@@ -64,11 +64,20 @@ import static java.time.temporal.ChronoField.DAY_OF_YEAR; ...@@ -64,11 +64,20 @@ import static java.time.temporal.ChronoField.DAY_OF_YEAR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR; import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.YEAR; import static java.time.temporal.ChronoField.YEAR;
import static java.time.temporal.ChronoField.YEAR_OF_ERA; 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.assertEquals;
import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNotNull;
import java.text.ParsePosition; 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.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAccessor;
...@@ -86,13 +95,17 @@ public class TestReducedParser extends AbstractTestPrinterParser { ...@@ -86,13 +95,17 @@ public class TestReducedParser extends AbstractTestPrinterParser {
private static final boolean LENIENT = false; private static final boolean LENIENT = false;
private DateTimeFormatter getFormatter0(TemporalField field, int width, int baseValue) { 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) { private DateTimeFormatter getFormatter0(TemporalField field, int minWidth, int maxWidth, int baseValue) {
return builder.appendValueReduced(field, minWidth, maxWidth, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); 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") @DataProvider(name="error")
Object[][] data_error() { Object[][] data_error() {
...@@ -243,6 +256,10 @@ public class TestReducedParser extends AbstractTestPrinterParser { ...@@ -243,6 +256,10 @@ public class TestReducedParser extends AbstractTestPrinterParser {
// Negative baseValue // Negative baseValue
{YEAR, 2, 4, -2005, "123", 0, strict(3, 123), lenient(3, 123)}, {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 @@ public class TestReducedParser extends AbstractTestPrinterParser { ...@@ -264,6 +281,21 @@ public class TestReducedParser extends AbstractTestPrinterParser {
} }
} }
@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 // Parsing tests for lenient mode
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
...@@ -282,6 +314,21 @@ public class TestReducedParser extends AbstractTestPrinterParser { ...@@ -282,6 +314,21 @@ public class TestReducedParser extends AbstractTestPrinterParser {
} }
} }
@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) { private void assertParsed(TemporalAccessor parsed, TemporalField field, Long value) {
if (value == null) { if (value == null) {
assertEquals(parsed, null, "Parsed Value"); assertEquals(parsed, null, "Parsed Value");
...@@ -334,6 +381,68 @@ public class TestReducedParser extends AbstractTestPrinterParser { ...@@ -334,6 +381,68 @@ public class TestReducedParser extends AbstractTestPrinterParser {
} }
} }
//-----------------------------------------------------------------------
// 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 // Class to structure the test data
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
......
...@@ -59,19 +59,15 @@ ...@@ -59,19 +59,15 @@
*/ */
package test.java.time.format; package test.java.time.format;
import java.text.ParsePosition;
import static java.time.temporal.ChronoField.YEAR; import static java.time.temporal.ChronoField.YEAR;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail; import static org.testng.Assert.fail;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.chrono.MinguoDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder; 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 java.time.temporal.TemporalField;
import org.testng.annotations.DataProvider; import org.testng.annotations.DataProvider;
...@@ -85,13 +81,17 @@ import test.java.time.temporal.MockFieldValue; ...@@ -85,13 +81,17 @@ import test.java.time.temporal.MockFieldValue;
public class TestReducedPrinter extends AbstractTestPrinterParser { public class TestReducedPrinter extends AbstractTestPrinterParser {
private DateTimeFormatter getFormatter0(TemporalField field, int width, int baseValue) { 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) { private DateTimeFormatter getFormatter0(TemporalField field, int minWidth, int maxWidth, int baseValue) {
return builder.appendValueReduced(field, minWidth, maxWidth, baseValue).toFormatter(locale).withDecimalStyle(decimalStyle); 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) @Test(expectedExceptions=DateTimeException.class)
public void test_print_emptyCalendrical() throws Exception { public void test_print_emptyCalendrical() throws Exception {
...@@ -192,6 +192,58 @@ public class TestReducedPrinter extends AbstractTestPrinterParser { ...@@ -192,6 +192,58 @@ public class TestReducedPrinter extends AbstractTestPrinterParser {
} }
} }
@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 { public void test_toString() throws Exception {
assertEquals(getFormatter0(YEAR, 2, 2, 2005).toString(), "ReducedValue(Year,2,2,2005)"); assertEquals(getFormatter0(YEAR, 2, 2, 2005).toString(), "ReducedValue(Year,2,2,2005)");
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册