Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8032052

Bug in handling millisecond pattern in new java.time DateTimeFormatterBuilder

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Duplicate
    • Icon: P3 P3
    • tbd
    • 8
    • core-libs

      FULL PRODUCT VERSION :
      java version "1.8.0-ea"
      Java(TM) SE Runtime Environment (build 1.8.0-ea-b119)
      Java HotSpot(TM) 64-Bit Server VM (build 25.0-b61, mixed mode)

      ADDITIONAL OS VERSION INFORMATION :
      Linux 3.8.0-35-generic #50~precise1-Ubuntu SMP Wed Dec 4 17:25:51 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux

      A DESCRIPTION OF THE PROBLEM :
      Demonstration of issue:

      DateTimeFormatter.ofPattern("yyyyMMddHHmmss" ).parse("20140115010203" , LocalDateTime::from); // Works as expected.
      DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").parse("20140115010203004", LocalDateTime::from); // Throws DateTimeParseException

      The issue appears to be that DateTimeFormatterBuilder does not adjust the subsequentWidth value of the base printer parser on processing the millisecond part of pattern.

      Here is a walk through of the issue.

      The DateTimeFormatterBuilder works through the elements of the pattern.
      Here we see the stack trace when the printer parser for the seconds part of the pattern is appended to the list of printer parsers:

      DateTimeFormatterBuilder.appendInternal(DateTimeFormatterBuilder$DateTimePrinterParser) line: 2008
      DateTimeFormatterBuilder.appendValue(DateTimeFormatterBuilder$NumberPrinterParser) line: 635
      DateTimeFormatterBuilder.appendValue(TemporalField, int) line: 446
      DateTimeFormatterBuilder.parseField(char, int, TemporalField) line: 1805
      DateTimeFormatterBuilder.parsePattern(String) line: 1604
      DateTimeFormatterBuilder.appendPattern(String) line: 1572
      DateTimeFormatter.ofPattern(String) line: 537

        To get to this point we had to pass through DateTimeFormatterBuilder.appendValue(NumberPrinterParser).
      This method increases the subsequentWidth value of the base printer parser (the year printer parser in our case) by the max-width of the seconds printer parser.

      So back when we first entered appendValue(NumberPrinterParser) for the year printer parser and set it as the base printer parser it had a subsequentWidth of 0.
      But as the method was called for each subsequent printer parser (corresponding to the month, day, hour, minute and second part of the pattern) this was increased until it became 10.

      yyyyMMddHHmmssSSS
          ^^^^^^^^^^ 10 characters.

      However for the milliseconds part of the pattern we go through different logic as shown by this stack trace:

      DateTimeFormatterBuilder.appendInternal(DateTimeFormatterBuilder$DateTimePrinterParser) line: 2008
      DateTimeFormatterBuilder.appendFraction(TemporalField, int, int, boolean) line: 688
      DateTimeFormatterBuilder.parseField(char, int, TemporalField) line: 1786
      DateTimeFormatterBuilder.parsePattern(String) line: 1604
      DateTimeFormatterBuilder.appendPattern(String) line: 1572
      DateTimeFormatter.ofPattern(String) line: 537

      Here we go throught appendFraction(TemporalField, int, int, boolean) and this does not adjust the subsequentWidth of the base parser.

      So the subsequentWidth value of the year parser ends up being 10 whether or not we're working with the pattern above that has a milliseconds portion or the one that does not.

      Now we can see how this causes the parse example shown above to fail...

      For the case where the pattern has no milliseconds portion the parsing of the year goes perfectly.

      In the year printer parser the first scan, to build up the long value representing the year, occurs and then effMaxWidth is recalculated to be 4 here:

      DateTimeFormatterBuilder$NumberPrinterParser.parse(DateTimeParseContext, CharSequence, int) line: 2662
      DateTimeFormatterBuilder$CompositePrinterParser.parse(DateTimeParseContext, CharSequence, int) line: 2211
      DateTimeFormatter.parseUnresolved0(CharSequence, ParsePosition) line: 2005
      DateTimeFormatter.parseResolved0(CharSequence, ParsePosition) line: 1938
      DateTimeFormatter.parse(CharSequence, TemporalQuery<T>) line: 1850

      Then the second scan occurs and the long value 2014 is built up. Then we successfully proceed through the remaining logic to the end of the method and call setValue(DateTimeParseContext, long, int, int) passing in the 2014 value:

      DateTimeFormatterBuilder$NumberPrinterParser.setValue(DateTimeParseContext, long, int, int) line: 2715
      DateTimeFormatterBuilder$NumberPrinterParser.parse(DateTimeParseContext, CharSequence, int) line: 2702
      DateTimeFormatterBuilder$CompositePrinterParser.parse(DateTimeParseContext, CharSequence, int) line: 2211

      Things don't go quite so well in the case where the pattern has a milliseconds portion.

      The first scan occurs as before but when it comes to recalculating effMaxWidth at the end of this scan parseLen has a value of 17 and subsequentWidth has a value of 10 and so effMaxWidth ends up as 7 rather than 4, i.e. were out by the length of the milliseconds portion of the pattern - that we saw was not taken into account in subsequentWidth.

      So here the second scan consumes 3 characters more than it should and builds up a year value of 2014011.

      And then at the end of this scan, instead of proceeding to the end of the method, we hit the logic that causes us to return ~position as parseLen is greater than minWidth.

      This causes us to bubble back up and first set the error index (as our position is negative):

      ParsePosition.setErrorIndex(int) line: 106
      DateTimeFormatter.parseUnresolved0(CharSequence, ParsePosition) line: 2007
      DateTimeFormatter.parseResolved0(CharSequence, ParsePosition) line: 1938
      DateTimeFormatter.parse(CharSequence, TemporalQuery<T>) line: 1850

      And then throw a DateTimeParseException exception:

      DateTimeParseException.<init>(String, CharSequence, int) line: 100
      DateTimeFormatter.parseResolved0(CharSequence, ParsePosition) line: 1948
      DateTimeFormatter.parse(CharSequence, TemporalQuery<T>) line: 1850

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      LocalDateTime dateTime = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").parse("20140115010203004", LocalDateTime::from);

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      A LocalDateTime reflecting the results of parsing "20140115010203004" according to the pattern "yyyyMMddHHmmssSSS" should be assigned to dateTime.
      ACTUAL -
      A DateTimeParseException is thrown by the parse logic.

      ERROR MESSAGES/STACK TRACES THAT OCCUR :
      Exception in thread "main" java.time.format.DateTimeParseException: Text '20140115010203004' could not be parsed at index 0
      at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1948)
      at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1850)
      at DateTimeFormatterTest.testMilliPattern(DateTimeFormatterTest.java:15)


      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      import java.time.LocalDateTime;
      import java.time.format.DateTimeFormatter;

      import org.junit.Test;

      public class DateTimeFormatterTest {
          @Test
          public void testNoMilliPattern() {
              DateTimeFormatter.ofPattern("yyyyMMddHHmmss").parse("20140115010203", LocalDateTime::from);
          }
          
          @Test
          public void testMilliPattern() {
              DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").parse("20140115010203004", LocalDateTime::from);
          }
      }
      ---------- END SOURCE ----------

            rriggs Roger Riggs
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: