JDK-8183913 : Regression: DateTimeFormatter.parse(String) miscalculates ChronoField.INSTANT_SECONDS
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.time
  • Affected Version: 9
  • Priority: P3
  • Status: Resolved
  • Resolution: Not an Issue
  • OS: generic
  • CPU: generic
  • Submitted: 2017-07-03
  • Updated: 2017-08-07
  • Resolved: 2017-07-06
Related Reports
Relates :  
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


Comments
Closing this as "not an issue" per Stephen's explanation.
06-07-2017

The test case appears to be wrong. 2009-11-15T00:00:00.000+01:00 is the same instant as 2009-11-14T23:00:00.000+00:00 yet the test case above expects 2009-11-15T01:00:00.000+00:00 Basically the offset has been added instead of subtracted. A much simpler test is: public static void main(String[] args) { TimeZone.setDefault(TimeZone.getTimeZone("Europe/Paris")); TemporalAccessor t = ISO_DATE_TIME_PARSER.parse("2009-11-15T00:00:00.000+01:00"); System.out.println(t); System.out.println(t.getLong(OFFSET_SECONDS)); System.out.println(t.getLong(INSTANT_SECONDS)); Instant i = Instant.ofEpochSecond(t.getLong(INSTANT_SECONDS)); System.out.println(i); System.out.println(i.atOffset(ZoneOffset.ofTotalSeconds(t.get(OFFSET_SECONDS)))); } which returns: 3600 1258239600 2009-11-14T23:00:00Z 2009-11-15T00:00+01:00 I can't see anything wrong in java.time
06-07-2017

The behavioral change was caused by the fix to JDK-8066982. On parsing, if "offset" is present, then INSTANT_SECONDS field is calculated with that offset. The spec for DateTimeFormatter has been updated with: --- If both date and time were parsed and either an offset or zone is present, the field ChronoField.INSTANT_SECONDS is created. If an offset was parsed then the offset will be combined with the LocalDateTime to form the instant, with any zone ignored. If a ZoneId was parsed without an offset then the zone will be combined with the LocalDateTime to form the instant using the rules of ChronoLocalDateTime.atZone(ZoneId). ---
06-07-2017

To reproduce the issue run the attached test case. JDK 8u131 - pass JDK 9-ea+174 - fail Output on JDK 8u131: 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". Output on JDK 9-ea+174: 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!
06-07-2017