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

Regression: DateTimeFormatter.parse(String) miscalculates ChronoField.INSTANT_SECONDS

    XMLWordPrintable

Details

    • generic
    • generic

    Description

      FULL PRODUCT VERSION :
      java version "9"
      Java(TM) SE Runtime Environment (build 9+176)
      Java HotSpot(TM) 64-Bit Server VM (build 9+176, mixed mode)

      ADDITIONAL OS VERSION INFORMATION :
      macOS 10.12.5

      A DESCRIPTION OF THE PROBLEM :
      getLong(ChronoField.INSTANT_SECONDS) of the TemporalAccessor returned by DateTimeFormatter.parse(String) returns the wrong value.

      It appears that getLong(ChronoField.OFFSET_SECONDS) is subtracted from that value erroneously.

      The code below worked as expected in Java 8 but produces invalid values in Java 9.

      I'd appreciate any hints about how to write code that works with both Java 8 and Java 9 if this is not a bug.

      REGRESSION. Last worked in version 8u131

      ADDITIONAL REGRESSION INFORMATION:
      java version "1.8.0_131"
      Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
      Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      - Compile the code in the "Source code for an executable test case" field with Java 8.
      - Execute "java Java9TimeBug" using Java 8. This produces the "Expected Result".
      - Execute "java Java9TimeBug" using Java 9. This produces the "Actual Result".

      Compiling the code with Java 9 does not make a difference regarding "Actual Result".

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      instantSeconds: 1258243200
      offsetSeconds: 3600
      Everything fine for "2009-11-15T00:00:00.000+0100".

      instantSeconds: 1258243200
      offsetSeconds: 3600
      Everything fine for "2009-11-15T00:00:00.000+01:00".

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00.000+0000".

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00.000+00:00".

      instantSeconds: 1258243200
      offsetSeconds: -28800
      Everything fine for "2009-11-15T00:00:00.000-0800".

      instantSeconds: 1258243200
      offsetSeconds: -28800
      Everything fine for "2009-11-15T00:00:00.000-08:00".

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00.000Z".

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00Z".

      instantSeconds: 1258243200
      offsetSeconds: 3600
      Everything fine for "2009-11-15T00:00:00.017+0100".

      instantSeconds: 1258243200
      offsetSeconds: 3600
      Everything fine for "2009-11-15T00:00:00.017+01:00".

      instantSeconds: 1258243200
      offsetSeconds: 3600
      Everything fine for "2009-11-15T00:00:00+0100".

      instantSeconds: 1258243200
      offsetSeconds: 3600
      Everything fine for "2009-11-15T00:00:00+01:00".

      ACTUAL -
      instantSeconds: 1258239600
      offsetSeconds: 3600
      Input "2009-11-15T00:00:00.000+0100" returned "2009-11-15T00:00:00.000+00:00" instead of "2009-11-15T01:00:00.000+00:00"!
      Input "2009-11-15T00:00:00.000+0100" returned 1258243200000 instead of 1258246800000!

      instantSeconds: 1258239600
      offsetSeconds: 3600
      Input "2009-11-15T00:00:00.000+01:00" returned "2009-11-15T00:00:00.000+00:00" instead of "2009-11-15T01:00:00.000+00:00"!
      Input "2009-11-15T00:00:00.000+01:00" returned 1258243200000 instead of 1258246800000!

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00.000+0000".

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00.000+00:00".

      instantSeconds: 1258272000
      offsetSeconds: -28800
      Input "2009-11-15T00:00:00.000-0800" returned "2009-11-15T00:00:00.000+00:00" instead of "2009-11-14T16:00:00.000+00:00"!
      Input "2009-11-15T00:00:00.000-0800" returned 1258243200000 instead of 1258214400000!

      instantSeconds: 1258272000
      offsetSeconds: -28800
      Input "2009-11-15T00:00:00.000-08:00" returned "2009-11-15T00:00:00.000+00:00" instead of "2009-11-14T16:00:00.000+00:00"!
      Input "2009-11-15T00:00:00.000-08:00" returned 1258243200000 instead of 1258214400000!

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00.000Z".

      instantSeconds: 1258243200
      offsetSeconds: 0
      Everything fine for "2009-11-15T00:00:00Z".

      instantSeconds: 1258239600
      offsetSeconds: 3600
      Input "2009-11-15T00:00:00.017+0100" returned "2009-11-15T00:00:00.017+00:00" instead of "2009-11-15T01:00:00.017+00:00"!
      Input "2009-11-15T00:00:00.017+0100" returned 1258243200017 instead of 1258246800017!

      instantSeconds: 1258239600
      offsetSeconds: 3600
      Input "2009-11-15T00:00:00.017+01:00" returned "2009-11-15T00:00:00.017+00:00" instead of "2009-11-15T01:00:00.017+00:00"!
      Input "2009-11-15T00:00:00.017+01:00" returned 1258243200017 instead of 1258246800017!

      instantSeconds: 1258239600
      offsetSeconds: 3600
      Input "2009-11-15T00:00:00+0100" returned "2009-11-15T00:00:00.000+00:00" instead of "2009-11-15T01:00:00.000+00:00"!
      Input "2009-11-15T00:00:00+0100" returned 1258243200000 instead of 1258246800000!

      instantSeconds: 1258239600
      offsetSeconds: 3600
      Input "2009-11-15T00:00:00+01:00" returned "2009-11-15T00:00:00.000+00:00" instead of "2009-11-15T01:00:00.000+00:00"!
      Input "2009-11-15T00:00:00+01:00" returned 1258243200000 instead of 1258246800000!


      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      import java.text.ParseException;
      import java.time.Instant;
      import java.time.ZoneOffset;
      import java.time.ZonedDateTime;
      import java.time.format.DateTimeFormatterBuilder;
      import java.time.temporal.ChronoField;
      import java.time.temporal.TemporalAccessor;
      import java.util.Date;
      import java.util.regex.Matcher;
      import java.util.regex.Pattern;

      public class Java9TimeBug {
          public static class DateTimeFormatter {
              private static final String TIMEZONE_DATE_FORMAT_PATTERN = ".*([+-]\\d{2})(\\d{2})$";
              private static final int TIMEZONE_DATE_FORMAT_LENGTH = 5;
          
              private static final java.time.format.DateTimeFormatter ISO_DATE_TIME_PARSER =
                      new DateTimeFormatterBuilder()
                              .parseCaseInsensitive()
                              .append(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE)
                              .appendLiteral('T')
                              .appendValue(ChronoField.HOUR_OF_DAY, 2)
                              .appendLiteral(':')
                              .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
                              .appendLiteral(':')
                              .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
                              .optionalStart()
                              .appendFraction(ChronoField.MILLI_OF_SECOND, 3, 3, true)
                              .optionalEnd()
                              .appendOffset("+HH:MM", "Z")
                              .toFormatter()
                              .withZone(ZoneOffset.UTC);
          
              private static final java.time.format.DateTimeFormatter ISO_DATE_TIME_FORMATTER_WITH_MILLIS =
                      new DateTimeFormatterBuilder()
                              .parseCaseInsensitive()
                              .append(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE)
                              .appendLiteral('T')
                              .appendValue(ChronoField.HOUR_OF_DAY, 2)
                              .appendLiteral(':')
                              .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
                              .appendLiteral(':')
                              .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
                              .appendFraction(ChronoField.MILLI_OF_SECOND, 3, 3, true)
                              .appendOffset("+HH:MM", "+00:00")
                              .toFormatter()
                              .withZone(ZoneOffset.UTC);
          
              private final Pattern javaTimezonePattern = Pattern.compile(TIMEZONE_DATE_FORMAT_PATTERN);
          
              /**
               * This method parses a given string containing a dateTime in ISO8601 notation into a date.
               *
               * It can handle an optional millisecond fraction as well as timezone with either explicit '+/-HH:MM' or 'Z' UTC designator.
               *
               * @param dateTime a string containing a dateTime in ISO8601 notation.
               * @return the parsed date
               * @throws ParseException If the dateTime string is invalid.
               */
              public Date parse(String dateTime)
                  throws ParseException {
                  Matcher matcher = javaTimezonePattern.matcher(dateTime);
                  if(matcher.matches()) {
                      // correct +/-hhmm to +/-hh:mm
                      String hh = matcher.group(1);
                      String mm = matcher.group(2);
                      dateTime = dateTime.substring(0, dateTime.length() - TIMEZONE_DATE_FORMAT_LENGTH) + hh + ":" + mm;
                  }
                  TemporalAccessor temporal = ISO_DATE_TIME_PARSER.parse(dateTime);
                  long instantSeconds = temporal.getLong(ChronoField.INSTANT_SECONDS);
                  long offsetSeconds = temporal.getLong(ChronoField.OFFSET_SECONDS);
                  System.out.println("instantSeconds: "+instantSeconds);
                  System.out.println("offsetSeconds: "+offsetSeconds);
                  long seconds = instantSeconds + offsetSeconds;
                  long millis = seconds * 1000 + temporal.getLong(ChronoField.MILLI_OF_SECOND);
          
                  return new Date(millis);
              }
          
              /**
               * Returns a simplified ISO8601 datetime string in UTC.
               *
               * It will always contain a three-number millisecond field regardless if it is "needed"
               * (i.e. MILLI_OF_SECOND != 0) or not. The timezone of the date is always UTC but isn't using
               * the UTC designator 'Z'. Instead, it's using an explicit '+00:00'.
               *
               * That way a date formatted by this method will always have the same number of characters while creating output
               * that less intelligent date-parsing frameworks (incapable of the 'Z' notation) are still able to process.
               *
               * @param date the date to be formatted.
               * @return a simplified ISO8601 datetime string in UTC.
               */
              public String format(Date date) {
                  Instant instant = Instant.ofEpochMilli(date.getTime());
                  ZonedDateTime zoned = ZonedDateTime.ofInstant(instant, ZoneOffset.UTC);
                  return ISO_DATE_TIME_FORMATTER_WITH_MILLIS.format(zoned);
              }
          }

          public static void main(String[] args) throws Exception {
              DateTimeFormatter formatter = new DateTimeFormatter();
              testcases(formatter);
          }

          
          public static void testcases(DateTimeFormatter formatter) throws Exception {
              final String[] inputs = new String[]{
                  "2009-11-15T00:00:00.000+0100",
                  "2009-11-15T00:00:00.000+01:00",
                  "2009-11-15T00:00:00.000+0000",
                  "2009-11-15T00:00:00.000+00:00",
                  "2009-11-15T00:00:00.000-0800",
                  "2009-11-15T00:00:00.000-08:00",
                  "2009-11-15T00:00:00.000Z",
                  "2009-11-15T00:00:00Z",
                  "2009-11-15T00:00:00.017+0100",
                  "2009-11-15T00:00:00.017+01:00",
                  "2009-11-15T00:00:00+0100",
                  "2009-11-15T00:00:00+01:00",
              };

              final long[] expectedMillis = new long[] {
                  1258246800000L,
                  1258246800000L,
                  1258243200000L,
                  1258243200000L,
                  1258214400000L,
                  1258214400000L,
                  1258243200000L,
                  1258243200000L,
                  1258246800017L,
                  1258246800017L,
                  1258246800000L,
                  1258246800000L,
              };

              final String[] expectedResults = new String[]{
                  "2009-11-15T01:00:00.000+00:00",
                  "2009-11-15T01:00:00.000+00:00",
                  "2009-11-15T00:00:00.000+00:00",
                  "2009-11-15T00:00:00.000+00:00",
                  "2009-11-14T16:00:00.000+00:00",
                  "2009-11-14T16:00:00.000+00:00",
                  "2009-11-15T00:00:00.000+00:00",
                  "2009-11-15T00:00:00.000+00:00",
                  "2009-11-15T01:00:00.017+00:00",
                  "2009-11-15T01:00:00.017+00:00",
                  "2009-11-15T01:00:00.000+00:00",
                  "2009-11-15T01:00:00.000+00:00",
              };
              
              for(int i=0;i<inputs.length;i++) {
                  Date parsedDate = formatter.parse(inputs[i]);
                  String result = formatter.format(parsedDate);
                  check(inputs[i], result, expectedResults[i], parsedDate.getTime(), expectedMillis[i]);
              }
          }

          public static void check(String input, String result, String expectedResult, long resultMillis, long expectedMillis) {
              if(result.equals(expectedResult)) {
                  System.out.println("Everything fine for \""+input+"\".\n");
              } else {
                  System.out.println("Input \""+input+"\" returned \""+result+"\" instead of \""+expectedResult+"\"!");
                  System.out.println("Input \""+input+"\" returned "+resultMillis+" instead of "+expectedMillis+"!\n");
              }
          }
      }
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      unknown

      Attachments

        Issue Links

          Activity

            People

              naoto Naoto Sato
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              4 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: