为什么Java的DateTimeFormatter没有安全地解析/格式化Instants上的往返?

时间:2018-03-25 08:10:16

标签: java scala datetime-format java-time java.time.instant

问题

也许我在这里滥用API或遗漏了一些信息。这是API的错误还是设计缺陷?

跟进

最近我使用java.time(在scala&#39的REPL中但并不感兴趣)parseformat日期也包含偏移但没有时间像这样:"2018-03-24+01:00"

scala> :paste
// Entering paste mode (ctrl-D to finish)

import java.time._
import java.time.temporal._
import java.time.format._
import java.util._

val f = DateTimeFormatter.ISO_OFFSET_DATE
  .withLocale(Locale.GERMAN)
  .withZone(ZoneId.of("GMT"))

val t = f.parse("2018-03-24+01:00")

// Exiting paste mode, now interpreting.

import java.time._
import java.time.temporal._
import java.time.format._
import java.util._
f: java.time.format.DateTimeFormatter = ParseCaseSensitive(false)(Value(Year,4,10,EXCEEDS_PAD)'-'Value(MonthOfYear,2)'-'Value(DayOfMonth,2))Offset(+HH:MM:ss,'Z')
t: java.time.temporal.TemporalAccessor = {OffsetSeconds=3600},ISO,GMT resolved to 2018-03-24

到目前为止一切顺利,在尝试创建Instant.from(t)时,这将导致异常:

scala> Instant.from(t)
java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor: {OffsetSeconds=3600},ISO,GMT resolved to 2018-03-24 of type java.time.format.Parsed
  at java.time.Instant.from(Instant.java:378)
  ... 28 elided
Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
  at java.time.format.Parsed.getLong(Parsed.java:203)
  at java.time.Instant.from(Instant.java:373)
  ... 28 more

由于缺少必需的ChronoFields,这对我来说是预期的或至少是合理的。对此特定方案的修复可能是提供合理的默认时间,例如LocalDate.MIDNIGHT,并在Instant上调用parse后手动构建f

scala> :paste
// Entering paste mode (ctrl-D to finish)

val i = OffsetDateTime.of(
  LocalDate.from(t),
  LocalTime.MIDNIGHT,
  ZoneOffset.from(t)
).toInstant


// Exiting paste mode, now interpreting.

i: java.time.Instant = 2018-03-23T23:00:00Z

嗯,这解决了我目前的问题,但感到不满意。然后我想知道在使用时间保留的格式化程序时API是否能够进行往返。因此,我想出了可用的java.time.format.DateTimeFormatter实例列表,并将它们放在一个小的测试函数上来验证这一点:

val dfts = scala.collection.immutable.Map(
  "BASIC_ISO_DATE"       -> DateTimeFormatter.BASIC_ISO_DATE,
  "ISO_INSTANT"          -> DateTimeFormatter.ISO_INSTANT,
  "ISO_LOCAL_TIME"       -> DateTimeFormatter.ISO_LOCAL_TIME,
  "ISO_OFFSET_TIME"      -> DateTimeFormatter.ISO_OFFSET_TIME,
  "ISO_WEEK_DATE"        -> DateTimeFormatter.ISO_WEEK_DATE,
  "ISO_DATE"             -> DateTimeFormatter.ISO_DATE,
  "ISO_LOCAL_DATE"       -> DateTimeFormatter.ISO_LOCAL_DATE,
  "ISO_OFFSET_DATE"      -> DateTimeFormatter.ISO_OFFSET_DATE,
  "ISO_ORDINAL_DATE"     -> DateTimeFormatter.ISO_ORDINAL_DATE,
  "ISO_ZONED_DATE_TIME"  -> DateTimeFormatter.ISO_ZONED_DATE_TIME,
  "ISO_DATE_TIME"        -> DateTimeFormatter.ISO_DATE_TIME,
  "ISO_LOCAL_DATE_TIME"  -> DateTimeFormatter.ISO_LOCAL_DATE_TIME,
  "ISO_OFFSET_DATE_TIME" -> DateTimeFormatter.ISO_OFFSET_DATE_TIME,
  "ISO_TIME"             -> DateTimeFormatter.ISO_TIME,
  "RFC_1123_DATE_TIME"   -> DateTimeFormatter.RFC_1123_DATE_TIME
)

def test(f: DateTimeFormatter) = scala.util.Try {
  Instant.from(f.parse(f.format(Instant.now)))
}

dfts.mapValues(test)
  .mapValues(_.toString)
  .mapValues(_.replace("java.time.temporal.UnsupportedTemporalTypeException", "j.t.t.UTTE"))
  .map{ case (k,v) => f"$k%20s : $v" }
  .foreach(println)

这会产生以下输出:

            ISO_DATE : Failure(j.t.t.UTTE: Unsupported field: Year)
 ISO_ZONED_DATE_TIME : Failure(j.t.t.UTTE: Unsupported field: Year)
      BASIC_ISO_DATE : Failure(j.t.t.UTTE: Unsupported field: Year)
      ISO_LOCAL_TIME : Failure(j.t.t.UTTE: Unsupported field: HourOfDay)
    ISO_ORDINAL_DATE : Failure(j.t.t.UTTE: Unsupported field: Year)
      ISO_LOCAL_DATE : Failure(j.t.t.UTTE: Unsupported field: Year)
       ISO_DATE_TIME : Failure(j.t.t.UTTE: Unsupported field: Year)
         ISO_INSTANT : Success(2018-03-25T07:48:48.360Z)
 ISO_LOCAL_DATE_TIME : Failure(j.t.t.UTTE: Unsupported field: Year)
     ISO_OFFSET_TIME : Failure(j.t.t.UTTE: Unsupported field: HourOfDay)
ISO_OFFSET_DATE_TIME : Failure(j.t.t.UTTE: Unsupported field: Year)
  RFC_1123_DATE_TIME : Failure(j.t.t.UTTE: Unsupported field: DayOfMonth)
     ISO_OFFSET_DATE : Failure(j.t.t.UTTE: Unsupported field: Year)
            ISO_TIME : Failure(j.t.t.UTTE: Unsupported field: HourOfDay)
       ISO_WEEK_DATE : Failure(j.t.t.UTTE: Unsupported field: WeekBasedYear)

因此,只有ISO_INSTANT格式化程序当前正在工作,至少我希望它能够工作,因此能够在往返场景中工作。

1 个答案:

答案 0 :(得分:4)

您可以使用DateTimeFormatterBuilderdefine default values作为时间字段。代码是用Java编写的,但是将它移植到Scala应该很容易:

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // date and offset
    .append(DateTimeFormatter.ISO_OFFSET_DATE)
    // default value for hour
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    // default value for minute
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    // create formatter
    .toFormatter(Locale.GERMAN);

Instant instant = Instant.from(fmt.parse("2018-03-24+01:00")); // 2018-03-23T23:00:00Z

输入没有任何与时间相关的字段(小时,分钟等),因此在解析时,您需要为自己设置时间,方法是使用特定的LocalTime(如你做了),或者通过定义上面的默认值。

没有"时间保留"格式化。将日期格式化为字符串时,它仅打印格式化程序中配置的字段。将此字符串解析回日期/时间类时,您需要以某种方式添加缺少的字段。

尝试所有内置格式化程序的代码实际上无法格式化 Instant,而不是解析它。这是因为Instant只表示来自Unix纪元的计数,并且任何内置格式化程序(ISO_INSTANT除外)都试图从中获取与日期/时间相关的字段(例如年,月,日,小时,分钟等,但Instant没有它的概念,因为它只代表了一个纳秒的时间,它需要附加到一个时区来获得日期/时间相关字段 - 更多详细信息:https://stackoverflow.com/a/27483371/9552515

如果您认为这是一个错误,go on and register it at Oracle。 但我相信这是设计的。我上面链接的答案来自java.time API的创建者,并加强了这种行为,并且他还解释了这一点this comment。如果这是一个设计缺陷,这是一个意见问题(和IMO,它不是)。话虽如此,我不认为这可能会改变。