JDK-8223773 : DateTimeFormatter Fails to throw an Exception on Invalid HOUR_OF_AMPM
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.time
  • Affected Version: 8,11,12,13
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2019-05-09
  • Updated: 2019-06-27
  • Resolved: 2019-05-31
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 13
13 b24Fixed
Related Reports
CSR :  
Sub Tasks
JDK-8225062 :  
Description
ADDITIONAL SYSTEM INFORMATION :
Tested on 
JDK1.8.0_181 on Windows 10
Java 11.0.3-ojdkbuild on Ubuntu Windows Subsystem for Linux

A DESCRIPTION OF THE PROBLEM :
When parsing using a DateTimeFormmatterBuilder and including both CLOCK_HOUR_OF_AMPM or HOUR_OF_AMPM and providing a date outside of their respective ranges, the parser will fail to throw an exception.  According to the spec, both of these should throw a parse exception when the input is out of bounds.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Parse "2007-10-22 12:00 PM" with HOUR_OF_AMPM
2. Parse "2007-10-22 12:00 PM" with CLOCK_HOUR_OF_AMPM
1. Parse "2007-10-22 00:00 PM" with HOUR_OF_AMPM
2. Parse "2007-10-22 00:00 PM" with CLOCK_HOUR_OF_AMPM

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
1. DateTimeParseException
2. Noon
3. Noon
4. DateTimeParseException
ACTUAL -
1. Midnight of the Next Day
2. Noon
3. Noon
4. Noon

---------- BEGIN SOURCE ----------
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;

public class Main {

    public static void main(String[] args) {
        // Expected DateTimeParse Error - Got Midnight of Next Day
        String input = "2007-10-22 12:00 PM";
        DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
                .parseStrict()
                .appendValue(ChronoField.YEAR_OF_ERA)
                .appendLiteral('-')
                .appendValue(ChronoField.MONTH_OF_YEAR)
                .appendLiteral('-')
                .appendValue(ChronoField.DAY_OF_MONTH)
                .appendLiteral(' ')
                .appendValue(ChronoField.HOUR_OF_AMPM,2)
                .appendLiteral(':')
                .appendValue(ChronoField.MINUTE_OF_HOUR,2)
                .appendLiteral(' ')
                .appendText(ChronoField.AMPM_OF_DAY);
        DateTimeFormatter formatter = builder.toFormatter(Locale.US);
        TemporalAccessor accessor = formatter.parse(input);
        LocalDateTime localDateTime = LocalDateTime.from(accessor);
        System.out.println(localDateTime.toString());

        // Expected Noon - Got Noon
        input = "2007-10-22 12:00 PM";
        builder = new DateTimeFormatterBuilder()
                .parseStrict()
                .appendValue(ChronoField.YEAR_OF_ERA)
                .appendLiteral('-')
                .appendValue(ChronoField.MONTH_OF_YEAR)
                .appendLiteral('-')
                .appendValue(ChronoField.DAY_OF_MONTH)
                .appendLiteral(' ')
                .appendValue(ChronoField.CLOCK_HOUR_OF_AMPM,2)
                .appendLiteral(':')
                .appendValue(ChronoField.MINUTE_OF_HOUR,2)
                .appendLiteral(' ')
                .appendText(ChronoField.AMPM_OF_DAY);
        formatter = builder.toFormatter(Locale.US);
        accessor = formatter.parse(input);
        localDateTime = LocalDateTime.from(accessor);
        System.out.println(localDateTime.toString());



        // Expected Noon - Got Noon
        input = "2007-10-22 00:00 PM";
        builder = new DateTimeFormatterBuilder()
                .parseStrict()
                .appendValue(ChronoField.YEAR_OF_ERA)
                .appendLiteral('-')
                .appendValue(ChronoField.MONTH_OF_YEAR)
                .appendLiteral('-')
                .appendValue(ChronoField.DAY_OF_MONTH)
                .appendLiteral(' ')
                .appendValue(ChronoField.HOUR_OF_AMPM)
                .appendLiteral(':')
                .appendValue(ChronoField.MINUTE_OF_HOUR)
                .appendLiteral(' ')
                .appendText(ChronoField.AMPM_OF_DAY);
        formatter = builder.toFormatter(Locale.US);
        accessor = formatter.parse(input);
        localDateTime = LocalDateTime.from(accessor);
        System.out.println(localDateTime.toString());



        // Expected DateTimeParse Error - Got Noon
        input = "2007-10-22 00:00 PM";
        builder = new DateTimeFormatterBuilder()
                .parseStrict()
                .appendValue(ChronoField.YEAR_OF_ERA)
                .appendLiteral('-')
                .appendValue(ChronoField.MONTH_OF_YEAR)
                .appendLiteral('-')
                .appendValue(ChronoField.DAY_OF_MONTH)
                .appendLiteral(' ')
                .appendValue(ChronoField.CLOCK_HOUR_OF_AMPM)
                .appendLiteral(':')
                .appendValue(ChronoField.MINUTE_OF_HOUR)
                .appendLiteral(' ')
                .appendText(ChronoField.AMPM_OF_DAY);
        formatter = builder.toFormatter(Locale.US);
        accessor = formatter.parse(input);
        localDateTime = LocalDateTime.from(accessor);
        System.out.println(localDateTime.toString());
    }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Use HOUR_OF_AMPM in place of both HOUR_OF_AMPM and CLOCK_HOUR_OF_AMPM for parsing.

FREQUENCY : always



Comments
Checking the range of HourOfAmPm with the range of AmPmOfDay is apparently incorrect in Parsed.java. Fixing it will enable throwing the exception in the first example. However the expectation for the last example is not correct as mentioned above. Will fix the first example case to throw the exception.
29-05-2019

There is some ambiguity/confusion between DateTimeFormatterBuilder.parseStrict and DateTimeFormatterBuilder.withResolverStyle(STRICT). They are different modes and have different purposes. DateTimeFormatterBuilder.parseStrict() is for "matching the text and sign styles". It does not control the resolver that handles consistency between and filling in missing values of fields. As noted above, the default resolver style is smart, allowing quite a bit of flexibility in filling in the missing values of the time. It correctly, accepts 12 and because it is PM sets the HOUR of day to 24, causing the rounding over to the start of the next day. If the withResolveStyle(Strict) is invoked, the exception that occurs reports that HOUR_OF_DAY is out of range (24). Caused by: java.time.DateTimeException: Invalid value for HourOfDay (valid values 0 - 23): 24 at java.base/java.time.temporal.ValueRange.checkValidIntValue(ValueRange.java:330) at java.base/java.time.temporal.ChronoField.checkValidIntValue(ChronoField.java:736) at java.base/java.time.format.Parsed.resolveTime(Parsed.java:561) at java.base/java.time.format.Parsed.resolveTimeLenient(Parsed.java:526) at java.base/java.time.format.Parsed.resolve(Parsed.java:254) at java.base/java.time.format.DateTimeParseContext.toResolved(DateTimeParseContext.java:331) at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2055) at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1877) at Main.main(Main.java:29) However, it should not have gotten that far. The checkValidValue at line j.t.f.Parsed:402 should have reported it out of range. The argument to HOUR_OF_AMPM.checkValidValue(ap); is incorrect; it should be "hap", not "ap".
28-05-2019

Looking at the code, it intentionally allows 12 in the first example, and it does seem the right behavior to me as it is in smart mode. The spec of HOUR_OF_AMPM should have the same range wrt STRICT/SMART description with CLOCK_HOUR_OF_AMPM.
13-05-2019

1) As per the Java API documentation at https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoField.html#HOUR_OF_AMPM : "The value is validated from 0 to 11 in strict and smart mode. " The default ResolverStyle is SMART, so the value should have been validated from 0 to 11 and an exception should have been thrown. The exception is only thrown for STRICT style when the code is changed to : TemporalAccessor accessor = formatter.withResolverStyle(ResolverStyle.STRICT).parse(input); So, this looks like a bug. JDK 8u212 - Fail JDK 13-ea + 17 - Fail 2) As per https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoField.html#CLOCK_HOUR_OF_AMPM : "The value is validated from 1 to 12 in strict mode and from 0 to 12 in smart mode." By default, the ResolverStyle is SMART, so CLOCK_HOUR_OF_AMPM is validated from 0 to 12 and it does not throw any exception. If the ResolverStyle is set to STRICT, then it throws an exception as expected.
13-05-2019