ZonedDateTime toString 与 ISO 8601 的兼容性

ZonedDateTime toString compatability with ISO 8601

我正在努力确保对我的 ZonedDateTime 对象调用 toString() 将符合 ISO-8601 格式。

toString() 方法的文档指出:

...The output is compatible with ISO-8601 if the offset and ID are the same

这是否意味着存在调用 zdt.getOffset() return 会有什么不同 zdt.getZone().getRules().getOffset(zdt.toInstant())

这似乎没有意义。

谁能提供一个偏移量和 ID 不相同的例子(即:toString() 不符合 ISO-8601),以便我更好地理解文档中的描述。

根据官方文档:

获取瞬间的偏移量很简单,因为每个瞬间只有一个有效偏移量。相比之下,获取本地日期时间的偏移量并不简单。一共有三种情况:

  • 正常,有一个有效偏移量。对于一年中的绝大多数时间,正常情况适用,本地日期时间只有一个有效偏移量。
  • 间隙,有效偏移量为零。这通常是由于 spring 夏令时从 "winter" 更改为 "summer" 导致时钟向前跳动的时间。在间隙中存在没有有效偏移量的本地日期时间值。
  • 重叠,有两个有效偏移量。这通常是由于秋季夏令时从 "summer" 更改为 "winter" 导致时钟拨慢的时候。在重叠中,有具有两个有效偏移量的本地日期时间值。

所以第二种和第三种情况是 toString() 不符合 ISO-8601 的情况。

这是完整的规格:

 * Outputs this date-time as a {@code String}, such as
 * {@code 2007-12-03T10:15:30+01:00[Europe/Paris]}.
 * <p>
 * The format consists of the {@code LocalDateTime} followed by the {@code ZoneOffset}.
 * If the {@code ZoneId} is not the same as the offset, then the ID is output.
 * The output is compatible with ISO-8601 if the offset and ID are the same.

Javadoc 规范指的是 ZonedDateTime 使用 ZoneOffset 而不是命名 ZoneId 构造的情况,因此偏移量和 ID 相同:

System.out.println(ZonedDateTime.now(ZoneId.of("Europe/Paris")));
// 2017-04-26T15:13:12.006+02:00[Europe/Paris]

System.out.println(ZonedDateTime.now(ZoneOffset.ofHours(2)));
// 2017-04-26T15:13:12.016+02:00

可以看出,在第二种情况下,使用了 ZoneOffsettoString() 格式省略了末尾的方括号部分。通过省略该部分,结果与 ISO-8601 兼容。

boolean iso8601Compatible = zdt.getZone() instanceof ZoneOffset;

为了保证 ISO-8601 兼容输出使用 toOffsetDateTime():

String isoCompatible = zdt.toOffsetDateTime().toString();

或格式化程序。

the documentation中的例子是2007-12-03T10:15:30+01:00[Europe/Paris]。这恰好不符合 ISO,因为 ISO-8601 不包含 [Europe/Paris] 部分。这是由 java.time 开发人员在尽可能接近标准和仍然以明确的方式提供时区信息之间进行折衷而添加的。

所以真正的问题实际上可能是相反的:如果 ZonedDateTime.toString() 包含 ISO 不包含的时区信息,当 结果完全符合 ISO ? “如果偏移量和 ID 相同”是什么意思?这里我们要记住 ZoneOffsetZoneID 的子类,在 ZonedDateTime 中可以用作区域 ID。在这种情况下,偏移量和 ID 是相同的。否则他们不是。举个具体的例子,ZonedDateTime.now(ZoneOffset.ofHours(+2)).toString()可能会产生2017-04-26T15:04:59.828+02:00。这是完全 ISO 兼容的,因为区域被指定为 +02:00,这与偏移量相同。 ZonedDateTime.now(ZoneOffset.UTC).toString() 也给出格式为 2017-04-26T13:04:59.828Z 的内容。由于 Z 算作偏移量,因此这也是兼容的。

我认为在大多数情况下它不会很有用。如果您的区域只是一个偏移量,您通常更愿意使用 OffsetDateTime 而不是 ZonedDateTime,如果是这样,您当然不关心 ZonedDateTime.toString() 是否兼容 ISO。

我喜欢上面 JodaStephen 的最后评论 'or a formatter',因为无论数据如何,使用格式化程序都更可靠。 原因是 OffsetDateTime toString() 在第二个单元及以下没有值时跳过第二个单元部分,所以它最终给出 yyyy-MM-ddTHH:mmZ 而不是 yyyy-MM-ddTHH:mm:ssZ。如果其他系统需要静态格式并且没有适应性,那可能会造成麻烦。

下面是我用来模拟没有时间部分和有时间部分的两种情况的代码。

/**
 * This function is design to convert Date object from other system to ISO dateTime String 
 * which will be sent to another system. and using formatter to lock up the format
 * @param date java.util.Date
 * @return 'yyyy-MM-ddTHH:mm:ssZ' format ISO dateTime string
 */
public String formatIsoUtcDateTime(Date date) {
    if(null == date) {
        return null;
    }
    return ZonedDateTime.ofInstant(date.toInstant(), ZoneId.of("UTC"))
                        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
}

// no time portion with using formatter
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
Date date = sdf.parse("20171010");
System.out.println(formatIsoUtcDateTime(date));

// no time portion with using OffsetDateTime toString
System.out.println(ZonedDateTime.ofInstant(date.toInstant(), ZoneId.of("UTC")).toOffsetDateTime().toString());

// just current date and probably has at least millisecond, using formatter
System.out.println(formatIsoUtcDateTime(new Date()));

// just current date and probably has at least millisecond, using OffsetDateTime toString
// this will print yyyy-MM-ddTHH:mm:ss.SSSZ format
System.out.println(ZonedDateTime.ofInstant(new Date().toInstant(), ZoneId.of("UTC")).toOffsetDateTime().toString());