为什么 Java 8 DateTimeFormatter 允许在 ResolverStyle.STRICT 模式下使用不正确的月份值?

Why does Java 8 DateTimeFormatter allows an incorrect month value in ResolverStyle.STRICT mode?

为什么这个测试通过了,而月份值明明是无效的(13)?

@Test
public void test() {

    String format = "uuuuMM";

    String value = "201713";

     DateTimeFormatter.ofPattern(format).withResolverStyle(ResolverStyle.STRICT)
        .parse(value);

}

使用临时查询时,抛出预期的 DateTimeParseException

DateTimeFormatter.ofPattern(format).withResolverStyle(ResolverStyle.STRICT)
    .parse(value, YearMonth::from);

没有指定 TemporalQuery 会发生什么?


编辑:13 值似乎是一个特殊值,正如我从 ΦXocę 웃 Пepeúpa ツ 的回答中了解到的(参见 Undecimber)。

但是即使有另一个值也不会抛出异常,比如 50:

@Test
public void test() {

    String format = "uuuuMM";

    String value = "201750";

     DateTimeFormatter.ofPattern(format).withResolverStyle(ResolverStyle.STRICT)
        .parse(value);

}

第 13 个月称为:Undecimber

我们许多人使用的 gregorian 日历仅允许 12 个月,但 java 包括对允许 13 个月的日历的支持几个月所以这取决于你在谈论什么日历系统

比如MONTH字段的实际最大值在某些年份是12,在其他年份是13希伯来语 日历系统。所以第 13 个月有效

在没有给定 TemporalQuery 的情况下调用 parse 时没有抛出异常有点奇怪。单参数 parse 方法的一些文档:

This parses the entire text producing a temporal object. It is typically more useful to use parse(CharSequence, TemporalQuery). The result of this method is TemporalAccessor which has been resolved, applying basic validation checks to help ensure a valid date-time.

请注意,它说“通常使用 parse(CharSequence, TemporalQuery) 更有用”。在您的示例中,parse 返回一个 java.time.format.Parsed 对象,该对象除了创建不同的 TemporalAccessor.

外并没有真正用于任何其他用途

请注意,如果您尝试从返回值创建 YearMonth,则会抛出异常:

YearMonth.from(DateTimeFormatter.ofPattern(format)
    .withResolverStyle(ResolverStyle.STRICT).parse(value));

投掷

 Exception in thread "main" java.time.DateTimeException: Unable to obtain YearMonth from TemporalAccessor: {Year=2017, MonthOfYear=50},ISO of type java.time.format.Parsed
    at java.time.YearMonth.from(YearMonth.java:263)
    at anl.nfolds.Test.main(Test.java:21)
Caused by: java.time.DateTimeException: Invalid value for MonthOfYear (valid values 1 - 12): 50
    at java.time.temporal.TemporalAccessor.get(TemporalAccessor.java:224)
    at java.time.YearMonth.from(YearMonth.java:260)
... 1 more

Parsed 的文档:

A store of parsed data.

This class is used during parsing to collect the data. Part of the parsing process involves handling optional blocks and multiple copies of the data get created to support the necessary backtracking.

Once parsing is completed, this class can be used as the resultant TemporalAccessor. In most cases, it is only exposed once the fields have been resolved.

Since:1.8

@implSpecThis class is a mutable context intended for use from a single thread. Usage of the class is thread-safe within standard parsing as a new instance of this class is automatically created for each parse and parsing is single-threaded

我在这里进行了一些调试,发现部分解析过程是根据格式化程序的时间顺序检查字段。

创建 DateTimeFormatter 时,会调用 by default it uses an IsoChronology, which is used to resolve the date fields. During this resolving phase, the method java.time.chrono.AbstractChronology::resolveDate

如果您查看 source,您将看到以下逻辑:

if (fieldValues.containsKey(YEAR)) {
    if (fieldValues.containsKey(MONTH_OF_YEAR)) {
        if (fieldValues.containsKey(DAY_OF_MONTH)) {
            return resolveYMD(fieldValues, resolverStyle);
        }
....
return null;

由于输入只有字段,fieldValues.containsKey(DAY_OF_MONTH)returnsfalse,方法 returns null 并且没有进行其他检查,如您在 Parsed class.

中所见

因此,在没有 TemporalQuery 的情况下解析 201750201713 时,由于上述逻辑和 parse 方法 [=75],因此不会进行额外的检查=]一个java.time.format.Parsed对象,如下代码所示:

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("uuuuMM").withResolverStyle(ResolverStyle.STRICT);
TemporalAccessor parsed = fmt.parse("201750");
System.out.println(parsed.getClass());
System.out.println(parsed);

输出为:

class java.time.format.Parsed
{Year=2017, MonthOfYear=50},ISO

请注意,返回对象的类型是 java.time.format.Parsed 并且打印它会显示已解析的字段(年和月)。

当您使用 TemporalQuery 调用 parse 时,Parsed 对象被传递给查询并验证其字段(当然这取决于查询,但是API 内置的总是有效)。

YearMonth::from 的情况下,它使用相应的 ChronoField 检查年份和月份是否有效(MONTH_OF_YEAR and YEAR) and the month field accepts only values from 1 to 12

这就是为什么仅调用 parse(value) 不会抛出异常,但使用 TemporalQuery 调用会抛出异常的原因。


当所有日期字段(年、月、日)都存在时,检查上面的逻辑:

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("uuuuMMdd").withResolverStyle(ResolverStyle.STRICT);
fmt.parse("20175010");

这抛出:

Exception in thread "main" java.time.format.DateTimeParseException: Text '20175010' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 50

由于所有日期字段都存在,fieldValues.containsKey(DAY_OF_MONTH) returns true 现在它检查它是否是有效日期(使用 resolveYMD method)。