带有可选模式的Java DateTimeFormatterBuilder导致DateTimeParseException

时间:2018-04-25 13:29:41

标签: java datetime-format

目标

为LocalDate实例提供灵活的解析器,可以使用以下格式之一处理输入:

  • YYYY
  • YYYYMM
  • YYYYMMDD

实施尝试

以下类尝试处理第一个和第二个模式。解析工作年份输入,但年+月导致下面列出的例外情况。

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;

public class DateTest {

    public static void main(String[] args) {
        DateTimeFormatter parser = new DateTimeFormatterBuilder()
        .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
        .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
        .appendPattern("yyyy")
        .optionalStart().appendPattern("MM").optionalEnd().toFormatter();

        System.out.println(parser.parse("2014", LocalDate::from)); // Works
        System.out.println(parser.parse("201411", LocalDate::from)); // Fails
    }
}

第二次解析()尝试会导致以下异常:

Exception in thread "main" java.time.format.DateTimeParseException: Text '201411' could not be parsed at index 0
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)

我认为我对可选部分模式如何工作的理解是缺乏的。我的目标是一个灵活格式的解析器甚至可以实现,还是我需要检查输入长度并从解析器列表中选择?一如既往,感谢帮助。

4 个答案:

答案 0 :(得分:2)

这是解决方案。您可以在appendPattern()中定义可能的模式。以及可选的默认值。

   DateTimeFormatter parser = new DateTimeFormatterBuilder()
            .appendPattern("[yyyy][yyyyMM][yyyyMMdd]")
            .optionalStart()
              .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
              .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
            .optionalEnd()
            .toFormatter();
    System.out.println(parser.parse("2014",LocalDate::from)); // Works
    System.out.println(parser.parse("201411",LocalDate::from)); // Works
    System.out.println(parser.parse("20141102",LocalDate::from)); // Works

输出

2014-01-01
2014-11-01
2014-11-02

答案 1 :(得分:2)

问题的真正原因是签名处理。您的输入没有任何符号,但解析器元素“yyyy”贪婪地解析尽可能多的数字并且期望一个正号,因为找到了超过四位数。

我的分析以两种不同的方式完成:

  • 调试(为了查看不明确的错误消息背后的内容)

  • 基于我的lib Time4J模拟另一个解析引擎中的行为,以获得更好的错误消息:

    ChronoFormatter<LocalDate> cf =
    ChronoFormatter
        .ofPattern(
            "yyyy[MM]",
            PatternType.THREETEN,
            Locale.ROOT,
            PlainDate.axis(TemporalType.LOCAL_DATE)
        )
        .withDefault(PlainDate.MONTH_AS_NUMBER, 1)
        .withDefault(PlainDate.DAY_OF_MONTH, 1)
        .with(Leniency.STRICT);
    System.out.println(cf.parse("201411")); 
    // java.text.ParseException: Positive sign must be present for big number.
    

您可以通过指示构建者一年只使用四位数来解决问题:

DateTimeFormatter parser =
    new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4)
        .optionalStart()
        .appendPattern("MM[dd]")
        .optionalEnd()
        .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
        .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
        .toFormatter();

System.out.println(parser.parse("2014", LocalDate::from)); // 2014-01-01
System.out.println(parser.parse("201411", LocalDate::from)); // 2014-11-01
System.out.println(parser.parse("20141130", LocalDate::from)); // 2014-11-30

注意构建器中默认元素的位置。它们不是在开始时调用,而是在最后调用,因为不幸的是,java.time中对默认元素的处理是位置敏感的。我还在第一个可选部分的内部添加了一个额外的可选部分。这个解决方案对我来说似乎更干净,而不是像Danila Zharenkov建议的那样使用3个可选部分的序列,因为后者也可以用更多的数字来解析相当不同的输入(可能滥用可选部分作为替换or-patterns特别是在宽松中解析)。

关于默认元素的位置敏感行为,这里引用API-documentation

  

在解析期间,将检查解析的当前状态。如果   指定的字段没有关联值,因为它还没有   在该点成功解析,然后指定的值为   注入解析结果。注射是立即的,因此   字段值对将对任何后续元素可见   格式化。因此,这种方法通常在结束时调用   助洗剂。

顺便说一句:在我的lib Time4J中,我还可以使用符号“|”定义真实或模式然后创建此格式化程序:

ChronoFormatter<LocalDate> cf =
    ChronoFormatter
        .ofPattern(
            "yyyyMMdd|yyyyMM|yyyy",
            PatternType.CLDR,
            Locale.ROOT,
            PlainDate.axis(TemporalType.LOCAL_DATE)
        )
        .withDefault(PlainDate.MONTH_AS_NUMBER, 1)
        .withDefault(PlainDate.DAY_OF_MONTH, 1)
        .with(Leniency.STRICT);

答案 2 :(得分:1)

在这部分代码中,您已经设置了月和日的值 .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1) .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) 然后,您尝试在代码中传递月份和年份的输入 System.out.println(parser.parse("201411", LocalDate::from));你已经设定了。

答案 3 :(得分:1)

您可以设置月和日的值,但会传递一个月和一年。这就是问题所在。

您可能想要使用:

.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
.parseDefaulting(ChronoField.YEAR_OF_ERA, ZonedDateTime.now().getYear())