从 SimpleDateFormat 移动到 DateTimeFormatter 时出现的问题

Problems when moving from SimpleDateFormat to DateTimeFormatter

过去几年我一直在成功使用 SimpleDateFormat。我用它构建了一堆时间实用程序 classes。

当我 运行 遇到 SimpleDateFormat (SDF) 不是线程安全的问题时,我花了最后几天重构这些实用程序 classes 以在内部使用 DateTimeFormatter (DTF) 现在。由于两个 classes 的时间模式几乎相同,所以这个 t运行sition 在当时看来是个好主意。

我现在在获取 EpochMillis 时遇到问题(自 1970-01-01T00:00:00Z 以来的毫秒数):虽然 SDF 会,例如将使用 HH:mm 解析的 10:30 解释为 1970-01-01T10:30:00Z,DTF 不这样做。 DTF 可以使用 10:30 来解析 LocalTime,但不能使用 ZonedDateTime 来解析 EpochMillis.

我理解 java.time 的对象遵循不同的理念; DateTimeZoned 对象分开保存。但是,为了让我的实用程序 class 像以前一样解释所有字符串,我需要能够为所有丢失的对象动态定义默认解析。我尝试使用

DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.parseDefaulting(ChronoField.YEAR, 1970);
builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
builder.append(DateTimeFormatter.ofPattern(pattern));

但这并不适用于所有模式。它似乎只允许 pattern 中未定义的参数的默认值。 有没有办法测试 pattern 中定义了哪些 ChronoField,然后有选择地添加默认值?

或者,我试过

TemporalAccessor temporal = formatter.parseBest(time,
        ZonedDateTime::from,
        LocalDateTime::from,
        LocalDate::from,
        LocalTime::from,
        YearMonth::from,
        Year::from,
        Month::from);
if ( temporal instanceof ZonedDateTime )
    return (ZonedDateTime)temporal;
if ( temporal instanceof LocalDateTime )
    return ((LocalDateTime)temporal).atZone(formatter.getZone());
if ( temporal instanceof LocalDate )
    return ((LocalDate)temporal).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof LocalTime )
    return ((LocalTime)temporal).atDate(LocalDate.of(1970, 1, 1)).atZone(formatter.getZone());
if ( temporal instanceof YearMonth )
    return ((YearMonth)temporal).atDay(1).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof Year )
    return ((Year)temporal).atMonth(1).atDay(1).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof Month )
    return Year.of(1970).atMonth((Month)temporal).atDay(1).atStartOfDay().atZone(formatter.getZone());

也没有涵盖所有情况。

启用动态日期/时间/日期时间/区域日期时间解析的最佳策略是什么?

Java-8-解法:

更改构建器中解析指令的顺序,以便默认指令全部发生在模式指令之后。

例如使用这个静态代码(好吧,你的方法将使用不同模式的基于实例的组合,根本不高效):

private static final DateTimeFormatter FLEXIBLE_FORMATTER;

static {
    DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
    builder.appendPattern("MM/dd");
    builder.parseDefaulting(ChronoField.YEAR_OF_ERA, 1970);
    builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
    builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
    builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
    builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
    builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
    FLEXIBLE_FORMATTER = builder.toFormatter();
}

原因:

方法 parseDefaulting(...) 以一种有趣的方式工作,即类似于嵌入式解析器。这意味着,如果尚未解析该字段,则此方法将为定义的字段注入默认值。后面的模式指令尝试解析相同的字段(此处:MONTH_OF_YEAR 用于模式 "MM/dd" 并输入“07/13”)但可能具有不同的值。如果是这样,那么复合解析器将中止,因为它发现相同字段的矛盾值并且无法解决冲突(解析值 7,但默认值 1)。

official API 包含以下通知:

During parsing, the current state of the parse is inspected. If the specified field has no associated value, because it has not been parsed successfully at that point, then the specified value is injected into the parse result. Injection is immediate, thus the field-value pair will be visible to any subsequent elements in the formatter. As such, this method is normally called at the end of the builder.

我们应该读作:

不要在同一字段的任何解析指令之前调用 parseDefaulting(...)

旁注 1:

您基于 parseBest(...) 的替代方法更糟糕,因为

  • 不涵盖所有缺分或只缺年(月日?)等的组合,默认值方案更灵活

  • 性能上不值得讨论

旁注 2:

我宁愿让整个实现顺序不敏感,因为这个细节对许多用户来说就像一个陷阱。并且可以通过为默认值选择基于映射的实现来避免这个陷阱,就像在我自己​​的时间库中所做的那样 Time4J where the order of default-value-instructions does not matter at all because injecting default values only happens after all fields have been parsed. Time4J also offers a dedicated answer to "What is the best strategy to enable dynamic date / time / date-time / zone-date-time parsing?" by offering a MultiFormatParser.

更新:

在 Java-8 中:使用 ChronoField.YEAR_OF_ERA 而不是 ChronoField.YEAR 因为模式包含字母 "y" (=year-of-era,与公历年)。否则,解析引擎除了解析的时代年份之外,还会注入预期的默认年份,并会发现冲突。一个真正的陷阱。就在昨天,我在我的时间库中为月份字段修复了一个 similar pitfall,它存在两个略有不同的变体。

我已经使用了新的 java.time 软件包,需要时间来适应它。但经过一段学习曲线后,我不得不说它绝对是非常全面和强大的解决方案,可能会取代 Joda 时间库和其他以前的解决方案。我编写了自己的实用程序,用于将字符串解析为 Date。我写了一篇总结文章,解释了我如何实现将未知格式的字符串解析为日期的功能。这可能会有所帮助。这是一篇文章的 link:Java 8 java.time package: parsing any string to date