JDK-8032491 : DateTimeFormatter fixed width adjacent value parsing does not match spec
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.time
  • Affected Version: 9
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • Submitted: 2014-01-22
  • Updated: 2014-07-29
  • Resolved: 2014-03-29
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8u20Fixed 9 b08Fixed
Related Reports
Relates :  
Description
The adjacent value parsing spec includes the phrase "If adjacent parsing is active, then parsing must match exactly the specified number of digits in both strict and lenient modes." in the method appendValue(TemporalField field, int width). Unfortunately, the spec is not correctly implemented when the first field in an adjacent parsing set is fixed width.

In strict mode, the first field in an adjacent parsing set uses its fixed width correctly producing the correct result. However it uses an inefficient double parse internally.

In lenient mode the first field in an adjacent parsing set is converted to be variable width producing the wrong result, and not conforming to the spec.

This can be noticed using a pattern of HHmm'9' which parses in strict mode but fails in lenient mode.

Discovered via JDK-8031085.
Comments
Patch for issue (second version) diff --git a/src/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/share/classes/java/time/format/DateTimeFormatterBuilder.java --- a/src/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -2595,8 +2595,9 @@ return value; } - boolean isFixedWidth() { - return subsequentWidth == -1; + boolean isFixedWidth(DateTimeParseContext context) { + return subsequentWidth == -1 || + (subsequentWidth > 0 && minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE); } @Override @@ -2625,12 +2626,12 @@ return ~position; } } - int effMinWidth = (context.isStrict() || isFixedWidth() ? minWidth : 1); + int effMinWidth = (context.isStrict() || isFixedWidth(context) ? minWidth : 1); int minEndPos = position + effMinWidth; if (minEndPos > length) { return ~position; } - int effMaxWidth = (context.isStrict() || isFixedWidth() ? maxWidth : 9) + Math.max(subsequentWidth, 0); + int effMaxWidth = (context.isStrict() || isFixedWidth(context) ? maxWidth : 9) + Math.max(subsequentWidth, 0); long total = 0; BigInteger totalBig = null; int pos = position; @@ -2855,6 +2856,13 @@ this.subsequentWidth + subsequentWidth); } + boolean isFixedWidth(DateTimeParseContext context) { + if (context.isStrict() == false) { + return false; + } + return super.isFixedWidth(context); + } + @Override public String toString() { return "ReducedValue(" + field + "," + minWidth + "," + maxWidth + "," + (baseDate != null ? baseDate : baseValue) + ")"; diff --git a/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java b/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java --- a/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java +++ b/test/java/time/tck/java/time/format/TCKDateTimeFormatterBuilder.java @@ -60,11 +60,14 @@ package tck.java.time.format; import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; import static java.time.temporal.ChronoField.YEAR; import static org.testng.Assert.assertEquals; +import java.text.ParsePosition; import java.time.LocalDate; import java.time.YearMonth; import java.time.ZoneOffset; @@ -73,6 +76,7 @@ import java.time.format.SignStyle; import java.time.format.TextStyle; import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -728,4 +732,152 @@ return LocalDate.of(y, m, d); } + //----------------------------------------------------------------------- + @Test + public void test_adjacent_strict_firstFixedWidth() throws Exception { + // succeeds because both number elements are fixed width + DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("12309", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 5); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + } + + @Test + public void test_adjacent_strict_firstVariableWidth_success() throws Exception { + // succeeds greedily parsing variable width, then fixed width, to non-numeric Z + DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('Z').toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("12309Z", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 6); + assertEquals(parsed.getLong(HOUR_OF_DAY), 123L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 9L); + } + + @Test + public void test_adjacent_strict_firstVariableWidth_fails() throws Exception { + // fails because literal is a number and variable width parse greedily absorbs it + DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("12309", pp); + assertEquals(pp.getErrorIndex(), 5); + assertEquals(parsed, null); + } + + @Test + public void test_adjacent_lenient() throws Exception { + // succeeds because both number elements are fixed width even in lenient mode + DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("12309", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 5); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + } + + @Test + public void test_adjacent_lenient_firstVariableWidth_success() throws Exception { + // succeeds greedily parsing variable width, then fixed width, to non-numeric Z + DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('Z').toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("12309Z", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 6); + assertEquals(parsed.getLong(HOUR_OF_DAY), 123L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 9L); + } + + @Test + public void test_adjacent_lenient_firstVariableWidth_fails() throws Exception { + // fails because literal is a number and variable width parse greedily absorbs it + DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("12309", pp); + assertEquals(pp.getErrorIndex(), 5); + assertEquals(parsed, null); + } + + //----------------------------------------------------------------------- + @Test + public void test_adjacent_strict_fractionFollows() throws Exception { + // succeeds because hour/min are fixed width + DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 0, 3, false).toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("1230567", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 7); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + assertEquals(parsed.getLong(NANO_OF_SECOND), 567_000_000L); + } + + @Test + public void test_adjacent_strict_fractionFollows_2digit() throws Exception { + // succeeds because hour/min are fixed width + DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 0, 3, false).toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("123056", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 6); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + assertEquals(parsed.getLong(NANO_OF_SECOND), 560_000_000L); + } + + @Test + public void test_adjacent_strict_fractionFollows_0digit() throws Exception { + // succeeds because hour/min are fixed width + DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 0, 3, false).toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("1230", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 4); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + // assertEquals(parsed.getLong(NANO_OF_SECOND), 0L); + } + + @Test + public void test_adjacent_lenient_fractionFollows() throws Exception { + // succeeds because hour/min are fixed width + DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 3, 3, false).toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("1230567", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 7); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + assertEquals(parsed.getLong(NANO_OF_SECOND), 567_000_000L); + } + + @Test + public void test_adjacent_lenient_fractionFollows_2digit() throws Exception { + // succeeds because hour/min are fixed width + DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 3, 3, false).toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("123056", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 6); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + assertEquals(parsed.getLong(NANO_OF_SECOND), 560_000_000L); + } + + @Test + public void test_adjacent_lenient_fractionFollows_0digit() throws Exception { + // succeeds because hour/min are fixed width + DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 3, 3, false).toFormatter(Locale.UK); + ParsePosition pp = new ParsePosition(0); + TemporalAccessor parsed = f.parseUnresolved("1230", pp); + assertEquals(pp.getErrorIndex(), -1); + assertEquals(pp.getIndex(), 4); + assertEquals(parsed.getLong(HOUR_OF_DAY), 12L); + assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L); + // assertEquals(parsed.getLong(NANO_OF_SECOND), 0L); + } + } diff --git a/test/java/time/test/java/time/format/TestReducedParser.java b/test/java/time/test/java/time/format/TestReducedParser.java --- a/test/java/time/test/java/time/format/TestReducedParser.java +++ b/test/java/time/test/java/time/format/TestReducedParser.java @@ -354,7 +354,7 @@ {"yyMMdd", "200703", STRICT, 0, 6, 2020, 7, 3}, {"ddMMyy", "230714", LENIENT, 0, 6, 2014, 7, 23}, {"ddMMyy", "230714", STRICT, 0, 6, 2014, 7, 23}, - {"ddMMyy", "25062001", LENIENT, 0, 8, 2001, 20, 2506}, + {"ddMMyy", "25062001", LENIENT, 0, 8, 2001, 6, 25}, {"ddMMyy", "25062001", STRICT, 0, 6, 2020, 6, 25}, {"ddMMy", "27052002", LENIENT, 0, 8, 2002, 5, 27}, {"ddMMy", "27052002", STRICT, 0, 8, 2002, 5, 27},
24-01-2014