从SimpleDateFormat移动到DateTimeFormatter时出现问题

时间:2016-11-11 23:42:44

标签: java simpledateformat

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

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

我现在遇到问题EpochMillis(自1970-01-01T00:00:00Z以来的毫秒数):虽然SDF会例如将使用10:30解析的HH:mm解释为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));

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

或者,我试过

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

也未涵盖所有案件。

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

2 个答案:

答案 0 :(得分:3)

<强>的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(...)以一种有趣的方式工作,就像嵌入式解析器一样。这意味着,如果尚未解析该字段,则此方法将为定义的字段注入默认值。后面的模式指令试图解析相同的字段(这里:模式“MM / dd”的MONTH_OF_YEAR和输入“07/13”),但可能有不同的值。如果是,则复合解析器将中止,因为它已找到相同字段的矛盾值并且无法解决冲突(解析值7,但默认值为1)。

official API包含以下声明:

  

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

我们应该把它读作:

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

旁注1:

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

  • 它不包括缺少分钟或仅缺少年份的所有组合(MonthDay?)等。默认值解决方案更灵活。

  • 表面上不值得讨论。

旁注2:

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

<强>更新

在Java-8中:使用ChronoField.YEAR_OF_ERA代替ChronoField.YEAR,因为该模式包含字母“y”(=年代,与预感格里高利年不同)。否则解析引擎除了解析年代之外还将注入默认年份,并且会发现冲突。一个真正的陷阱。就在昨天,我在我的时间库中为月份字段修复了similar pitfall,该字段存在两种略有不同的变体。

答案 1 :(得分:0)

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