具有灵活后备值的java.time DateTimeFormatter解析

alr*_*alr 5 java time datetime datetime-parsing java-time

我正在尝试将一些代码从joda时间移植到Java时间。

JodaTime可以这样指定当年的后备值

parser.withDefaultYear((new DateTime(DateTimeZone.UTC)).getYear()).parseDateTime(text);
Run Code Online (Sandbox Code Playgroud)

不管解析器的外观如何(如果包含一年或不包含一年),都将对其进行解析。

那里的java.time变得更加严格。即使有DateTimeFormatterBuilder.parseDefaulting()允许您指定后备方法的方法,也只有在您要解析的日期中指定该特定字段或将其标记为可选时,该方法才有效。

如果您对传入的日期格式没有任何控制权(由用户提供),则这将使调用时变得非常困难parseDefaulting

是否有任何解决方法,可以在其中指定通用回退日期(如果未指定其值,则格式化程序将使用其值)之类的东西,或者在格式化程序中指定后,如何配置未使用的回退值?

以下是最小,完整和可验证的示例。

public static DateTimeFormatter ofPattern(String pattern) {
    return new DateTimeFormatterBuilder()
        .appendPattern(pattern)
        .parseDefaulting(ChronoField.YEAR, 1970)
        .toFormatter(Locale.ROOT);
}

public void testPatterns() {
    // works
    assertThat(LocalDate.from(ofPattern("MM/dd").parse("12/06")).toString(), is("1970-12-06"));
    assertThat(LocalDate.from(ofPattern("uuuu/MM/dd").parse("2018/12/06")).toString(), is("2018-12-06"));
    // fails with exception, as it uses year of era
    assertThat(LocalDate.from(ofPattern("yyyy/MM/dd").parse("2018/12/06")).toString(), is("2018-12-06"));
}
Run Code Online (Sandbox Code Playgroud)

所需的结果:测试应解析字符串并通过(“呈绿色”)。

观察到的结果:测试的最后一行引发异常,并显示以下消息和堆栈跟踪。

文本'2018/12/06'无法解析:发现冲突:1970年与2018年不同

Exception in thread "main" java.time.format.DateTimeParseException: Text '2018/12/06' could not be parsed: Conflict found: Year 1970 differs from Year 2018
    at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1959)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1820)
    at com.ajax.mypackage.MyTest.testPatterns(MyTest.java:33)
Caused by: java.time.DateTimeException: Conflict found: Year 1970 differs from Year 2018
    at java.base/java.time.chrono.AbstractChronology.addFieldValue(AbstractChronology.java:676)
    at java.base/java.time.chrono.IsoChronology.resolveYearOfEra(IsoChronology.java:620)
    at java.base/java.time.chrono.IsoChronology.resolveYearOfEra(IsoChronology.java:126)
    at java.base/java.time.chrono.AbstractChronology.resolveDate(AbstractChronology.java:463)
    at java.base/java.time.chrono.IsoChronology.resolveDate(IsoChronology.java:585)
    at java.base/java.time.chrono.IsoChronology.resolveDate(IsoChronology.java:126)
    at java.base/java.time.format.Parsed.resolveDateFields(Parsed.java:360)
    at java.base/java.time.format.Parsed.resolveFields(Parsed.java:266)
    at java.base/java.time.format.Parsed.resolve(Parsed.java:253)
    at java.base/java.time.format.DateTimeParseContext.toResolved(DateTimeParseContext.java:331)
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1994)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1816)
    ... 1 more
Run Code Online (Sandbox Code Playgroud)

mcg*_*gee 7

parseDefaulting 如果未找到该字段的值,即使对于不在模式中的字段,也将设置该字段的值,因此您最终可能会遇到解析结果中同时存在年份和年份的情况。

对我来说,最简单的解决方案是评论中建议的:使用正则表达式检查输入是否包含年份(或看起来像一个的东西,例如 4 位数字),或者通过检查输入的长度,然后创建格式化程序相应地(并且没有默认值)。例子:

if (input_without_year) {
    LocalDate d = MonthDay
                      .parse("12/06", DateTimeFormatter.ofPattern("MM/dd"))
                      .atYear(1970);
} else {
    // use formatter with year, without default values
}
Run Code Online (Sandbox Code Playgroud)

但是如果你想要一个通用的解决方案,恐怕它会更复杂。一种替代方法是解析输入并检查其中是否有任何年份字段。如果没有,那么我们将其更改为返回年份的默认值:

public static TemporalAccessor parse(String pattern, String input) {
    DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern, Locale.ROOT);
    final TemporalAccessor parsed = fmt.parse(input);
    // check year and year of era
    boolean hasYear = parsed.isSupported(ChronoField.YEAR);
    boolean hasYearEra = parsed.isSupported(ChronoField.YEAR_OF_ERA);
    if (!hasYear && !hasYearEra) {
        // parsed value doesn't have any year field
        // return another TemporalAccessor with default value for year
        // using year 1970 - change it to Year.now().getValue() for current year
        return withYear(parsed, 1970); // see this method's code below
    }
    return parsed;
}
Run Code Online (Sandbox Code Playgroud)

首先我们解析并得到一个TemporalAccessor包含所有解析字段的。然后我们检查它是否有 year 或 year-of-era 字段。如果它没有其中任何一个,我们将创建另一个TemporalAccessor具有某些默认值的年份。

在上面的代码中,我使用的是 1970,但您可以将其更改为您需要的任何内容。该withYear方法有一些重要的细节需要注意:

  • 我假设输入总是有月和日。如果不是这种情况,您可以更改下面的代码以使用它们的默认值
    • 要检查字段是否存在,请使用该isSupported方法
  • LocalDate.from 内部使用 aTemporalQuery,它反过来查询纪元日字段,但是当解析的对象没有年份时,它无法计算纪元日,所以我也在计算它

withYear方法如下:

public static TemporalAccessor withYear(TemporalAccessor t, long year) {
    return new TemporalAccessor() {

        @Override
        public boolean isSupported(TemporalField field) {
            // epoch day is used by LocalDate.from
            if (field == ChronoField.YEAR_OF_ERA || field == ChronoField.EPOCH_DAY) {
                return true;
            } else {
                return t.isSupported(field);
            }
        }

        @Override
        public long getLong(TemporalField field) {
            if (field == ChronoField.YEAR_OF_ERA) {
                return year;
                // epoch day is used by LocalDate.from
            } else if (field == ChronoField.EPOCH_DAY) {
                // Assuming the input always have month and day
                // If that's not the case, you can change the code to use default values as well,
                // and use MonthDay.of(month, day)
                return MonthDay.from(t).atYear((int) year).toEpochDay();
            } else {
                return t.getLong(field);
            }
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

现在这有效:

System.out.println(LocalDate.from(parse("MM/dd", "12/06"))); // 1970-12-06
System.out.println(LocalDate.from(parse("uuuu/MM/dd", "2018/12/06"))); // 2018-12-06
System.out.println(LocalDate.from(parse("yyyy/MM/dd", "2018/12/06"))); // 2018-12-06
Run Code Online (Sandbox Code Playgroud)

但我仍然相信第一个解决方案更简单。

选择

假设您总是在创建LocalDate,另一种选择是使用parseBest

public static LocalDate parseLocalDate(String pattern, String input) {
    DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern, Locale.ROOT);

    // try to create a LocalDate first
    // if not possible, try to create a MonthDay
    TemporalAccessor parsed = fmt.parseBest(input, LocalDate::from, MonthDay::from);

    LocalDate dt = null;

    // check which type was created by the parser
    if (parsed instanceof LocalDate) {
        dt = (LocalDate) parsed;
    } else if (parsed instanceof MonthDay) {
        // using year 1970 - change it to Year.now().getValue() for current year
        dt = ((MonthDay) parsed).atYear(1970);
    } // else etc... - do as many checkings you need to handle all possible cases

    return dt;
}
Run Code Online (Sandbox Code Playgroud)

该方法parseBest 接收一个TemporalQuery实例列表(或等效的方法引用,如from上面的方法)并尝试按顺序调用它们:在上面的代码中,它首先尝试创建一个LocalDate,如果不可能,则尝试创建一个MonthDay

然后我检查返回的类型并采取相应的行动。你可以扩展它来检查你想要的任意数量的类型,你也可以编写自己的TemporalQuery来处理特定情况。

有了这个,所有情况也适用:

System.out.println(parseLocalDate("MM/dd", "12/06")); // 1970-12-06
System.out.println(parseLocalDate("uuuu/MM/dd", "2018/12/06")); // 2018-12-06
System.out.println(parseLocalDate("yyyy/MM/dd", "2018/12/06")); // 2018-12-06
Run Code Online (Sandbox Code Playgroud)


Ole*_*.V. 5

我不确定您是否应该这样做,但我将其作为一种选择。

private static LocalDate defaults = LocalDate.of(1970, Month.JANUARY, 1);

private static LocalDate parseWithDefaults(String pattern, String dateString) {
    TemporalAccessor parsed 
            = DateTimeFormatter.ofPattern(pattern, Locale.ROOT).parse(dateString);
    LocalDate result = defaults;
    for (TemporalField field : ChronoField.values()) {
        if (parsed.isSupported(field) && result.isSupported(field)) {
            result = result.with(field, parsed.getLong(field));
        }
    }
    return result;
}
Run Code Online (Sandbox Code Playgroud)

我采用相反的方法:不是采用丢失的字段并将其调整为解析的对象,而是采用默认LocalDate对象并将其解析的字段调整为该对象。关于此工作原理的规则很复杂,所以恐怕我们可能会感到惊讶或惊讶。另外,在完全指定的日期(如2018/12/06)中,它使用13个字段,因此显然存在一些冗余。但是,我用您的三个测试示例进行了尝试:

    System.out.println(parseWithDefaults("MM/dd", "12/06"));
    System.out.println(parseWithDefaults("uuuu/MM/dd", "2018/12/06"));
    System.out.println(parseWithDefaults("yyyy/MM/dd", "2018/12/06"));
Run Code Online (Sandbox Code Playgroud)

它打印了预期的

1970-12-06
2018-12-06
2018-12-06
Run Code Online (Sandbox Code Playgroud)

进一步思考

听起来这部分软件是根据Joda-Time的这种特定行为设计的。因此,即使您正在从Joda迁移到java.time–您应该很高兴的迁移–如果是我,我还是考虑保留Joda-Time在这个特定角落。这不是最令人愉快的选择,尤其是因为Joda-time和java.time(据我所知)之间不存在直接转换,因此尤其如此。您将需要自己权衡利弊。