从 SimpleDateFormat 移动到 DateTimeFormatter 时的问题

dot*_*win 5 java simpledateformat

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

由于我遇到了SimpleDateFormat(SDF) 不是线程安全的问题,因此我在过去几天中将这些实用程序类重构为DateTimeFormatter现在内部使用(DTF)。由于两个班级的时间模式几乎相同,因此这种转变在当时似乎是个好主意。

我现在在获取EpochMillis(自 以来的毫秒数1970-01-01T00:00:00Z)时遇到问题:虽然 SDF 会例如解释10:30使用HH:mmas解析1970-01-01T10:30:00Z,但 DTF 不会做同样的事情。DTF可以使用10:30来解析LocalTime,但不是ZonedDateTime它需要获得EpochMillis

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

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));
Run Code Online (Sandbox Code Playgroud)

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

或者,我试过

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());
Run Code Online (Sandbox Code Playgroud)

这也没有涵盖所有情况。

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

Men*_*ild 4

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();
}
Run Code Online (Sandbox Code Playgroud)

原因:

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

官方API包含以下通知:

在解析期间,会检查解析的当前状态。如果指定字段没有关联值,因为此时尚未解析成功,则将指定值注入到解析结果中。注入是立即的,因此字段值对对于格式化程序中的任何后续元素都是可见的。因此,该方法通常在构建器末尾调用。

我们应该将其读作:

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

旁注1:

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

  • 它不涵盖所有缺少分钟或仅缺少年份(月日?)等的组合。默认值解决方案更加灵活。

  • 性能方面不值得讨论。

旁注2:

我宁愿让整个实现顺序不敏感,因为这个细节对许多用户来说就像一个陷阱。并且可以通过选择基于映射的默认值实现来避免这个陷阱,就像我自己的时间库Time4J中所做的那样,其中默认值指令的顺序根本不重要,因为注入默认值仅在所有字段都已完成之后才发生。被解析了。Time4J 还针对“启用动态日期/时间/日期-时间/区域-日期-时间解析的最佳策略是什么?”提供了专门的答案。通过提供MultiFormatParser

更新:

在 Java-8 中:使用ChronoField.YEAR_OF_ERAnot ,ChronoField.YEAR因为该模式包含字母“y”(=year-of-era,与 proleptic Gregorian Year 不同)。否则,除了解析的纪元之外,解析引擎还将注入预推的默认年份,并会发现冲突。一个真正的陷阱。就在昨天,我在我的时间库中修复了月份字段的类似陷阱,该陷阱存在两种略有不同的变体。