java.time DateTimeFormatter使用灵活的回退值进行解析

时间:2018-04-13 07:56:27

标签: java datetime time java-time datetime-parsing

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

JodaTime有可能像这样指定一年的回退值

parser.withDefaultYear((new DateTime(DateTimeZone.UTC)).getYear()).parseDateTime(text);

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

java.time在那里变得更加严格。即使有DateTimeFormatterBuilder.parseDefaulting()方法允许您指定回退,但只有在您要解析或标记为可选日期的特定字段时,此方法才有效。< / p>

如果您对用户提供的传入日期格式没有任何控制权,那么在拨打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"));
}

期望的结果:测试应解析字符串并传递(“变为绿色”)。

观察到的结果:测试的最后一行抛出了一个带有以下消息和堆栈跟踪的异常。

  

文字&#39; 2018/12/06&#39;无法解析:发现冲突: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

3 个答案:

答案 0 :(得分:4)

我怀疑你是否应该这样做,但我把它作为一种选择。

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;
}

我正在采取相反的方式:不是将缺失的字段调整到已解析的对象中,而是采用默认的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"));

它打印了预期的

1970-12-06
2018-12-06
2018-12-06

进一步思考

听起来有点像这部分软件是围绕Joda-Time的这种特殊行为而设计的。所以即使你是从Joda迁移到java.time - 你应该感到高兴的迁移 - 如果是我,我会考虑让Joda-Time保持这个特定的角落。这不是最令人愉快的选择,尤其是因为在Joda-time和java.time之间没有直接的转换(我知道)。你需要自己权衡利弊。

答案 1 :(得分:4)

如果找不到该字段,

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

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

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

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

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;
}

首先,我们解析并获取包含所有已解析字段的TemporalAccessor。然后我们检查它是否有年份或年份字段。如果它没有任何一个,我们创建另一个TemporalAccessor,其中包含一年的默认值。

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

  • 我假设输入总是有月和日。如果不是这种情况,您可以更改下面的代码以使用它们的默认值
    • 要检查字段是否存在,请使用isSupported方法
  • LocalDate.from internally uses a TemporalQuery,反过来queries the epoch-day field,但是当解析后的对象没有年份时,它无法计算时差日,所以我也在计算它

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);
            }
        }
    };
}

现在可行:

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

但我仍然认为第一种解决方案更简单。

替代

假设您始终创建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;
}

方法parseBest receives a list of TemporalQuery instances (or equivalent method references, as the from methods above)并尝试按顺序调用它们:在上面的代码中,首先尝试创建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

答案 2 :(得分:1)

您可以尝试我的lib Time4J的解析引擎作为一种增强/改进,然后使用以下代码在解析过程中生成java.time.LocalDate的实例:

static ChronoFormatter<LocalDate> createParser(String pattern) {
    return ChronoFormatter // maybe consider caching the immutable formatter per pattern
        .ofPattern(
            pattern,
            PatternType.CLDR,
            Locale.ROOT, // locale-sensitive patterns require another locale
            PlainDate.axis(TemporalType.LOCAL_DATE) // converts to java.time.LocalDate
        )
        .withDefault(PlainDate.YEAR, 1970)
        .with(Leniency.STRICT);
}

public static void main(String[] args) throws Exception {
    System.out.println(createParser("uuuu/MM/dd").parse("2018/12/06")); // 2018-12-06
    System.out.println(createParser("yyyy/MM/dd").parse("2018/12/06")); // 2018-12-06
    System.out.println(createParser("MM/dd").parse("12/06")); // 1970-12-06
}

这是因为 - 尽管有严格的解析模式(检查矛盾元素值 - 模式符号&#34; y&#34;将映射到&#34; u&#34;(预感格里高利年)只要没有时代符号&#34; G&#34;各自的历史时代元素。

关于替代格式引擎的许多其他功能,请参阅documentation。还可以使用专用元素语法或customized patterns的构建器。存在定义默认值的其他变体。您的Joda-default-code可能会以这种方式迁移(使用系统时区,但也很容易使用UTC):

parser.withDefaultSupplier( // also works if current year is changing
  PlainDate.YEAR,
  () -> SystemClock.inLocalView().today().getYear()
  // or: () -> SystemClock.inZonalView(ZonalOffset.UTC).getYear()
)

关于使用模式的另一个重要通知

Joda和java.time的模式语法不同。你知道这个事实吗?迁移时,您还必须转换模式:

  • y =&gt;你
  • Y =&gt; y
  • x =&gt; ÿ