【问题标题】:DateTimeFormatter parsing - timezone names and daylight savings overlap timesDateTimeFormatter 解析 - 时区名称和夏令时重叠时间
【发布时间】:2021-12-24 00:52:07
【问题描述】:

为了提高一些遗留代码的性能,我正在考虑用 java.time.format.DateTimeFormatter 替换 java.text.SimpleDateFormat。

执行的任务包括解析使用 java.util.Date.toString 序列化的日期/时间值。使用 SimpleDateFormat,可以将它们转换回原始时间戳(忽略小数秒),但是在尝试对 DateTimeFormatter 执行相同操作时遇到问题。

当使用任何一种格式进行格式化时,我的本地时区将显示为 CET 或 CEST,具体取决于夏令时是否对要格式化的时间有效。但是,在解析时,DateTimeFormatter 似乎将 CET 和 CEST 视为相同。

这会在夏令时结束时产生重叠问题。格式化时, 02:00:00 创建两次,时间间隔一小时,但使用 CEST 和 CET 时区名称 - 这很好。但在解析时,无法回收这种差异。

这是一个例子:

long msecPerHour = 3600000L;
long cet_dst_2016 = 1477778400000L;
DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ENGLISH);
ZoneId timezone = ZoneId.of("Europe/Berlin");
for (int hours = 0; hours < 6; ++hours) {
    long time = cet_dst_2016 + msecPerHour * hours;
    String formatted = formatter.format(Instant.ofEpochMilli(time).atZone(timezone));
    long parsedTime = Instant.from(formatter.parse(formatted)).toEpochMilli();
    System.out.println(formatted + ", diff: " + (parsedTime - time));
}

导致

Sun Oct 30 00:00:00 CEST 2016, diff: 0
Sun Oct 30 01:00:00 CEST 2016, diff: 0
Sun Oct 30 02:00:00 CEST 2016, diff: 0
Sun Oct 30 02:00:00 CET 2016, diff: -3600000
Sun Oct 30 03:00:00 CET 2016, diff: 0
Sun Oct 30 04:00:00 CET 2016, diff: 0

它表明第二次出现的 02:00:00,尽管时区名称不同,但被视为第一次出现。所以结果实际上是关闭了一小时。

显然,格式化字符串包含所有可用信息,SimpleDateFormat 解析实际上尊重了它。是否可以使用给定模式使用 DateTimeFormatter 进行格式化和解析往返?

【问题讨论】:

    标签: java java-time


    【解决方案1】:

    一般解决方法

    java.time 的主要作者 JodaStephen 在他的回答中展示了针对 CET 和 CEST(中欧时间和中欧夏令时间)情况的解决方法。我提出了一种解决方法,我相信它适用于标准时间和夏令时 (DST) 的不同缩写的所有时区。

    public static ZonedDateTime parse(String text) {
        ZonedDateTime result = ZonedDateTime.parse(text, FORMATTER);
    
        if (result.format(FORMATTER).equals(text)) {
            return result;
        }
    
        // Default we get the earlier offset at overlap,
        // so if it didn’t work, try the later offset
        result = result.withLaterOffsetAtOverlap();
        if (result.format(FORMATTER).equals(text)) {
            return result;
        }
    
        // As a last desperate attempt, try earlier offset explicitly 
        result = result.withEarlierOffsetAtOverlap();
        if (result.format(FORMATTER).equals(text)) {
            return result;
        }
    
        // Give up
        throw new IllegalArgumentException();
    }
    

    该方法可以使用任何带有时区名称或缩写的格式化程序,只要它应该提供与它解析的输入相同的格式化输出(例如,可选部分是禁止的)。我假设了一个与您等效的格式化程序:

    private static final DateTimeFormatter FORMATTER
            = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ROOT);
    

    您的问题在于毫秒值 1 477 789 200 000,它被格式化为 Sun Oct 30 02:00:00 CET 2016,然后解析为 1 477 785 600 000,相差 -3 600 000 毫秒。所以让我们试试我的方法。

    private static final ZoneId TIME_ZONE = ZoneId.of("Europe/Berlin");
    
        long trouble = 1_477_789_200_000L;
        String formatted = Instant.ofEpochMilli(trouble).atZone(TIME_ZONE).format(FORMATTER);
        ZonedDateTime zdt = parse(formatted);
        long parsedTime = zdt.toInstant().toEpochMilli();
        System.out.println(formatted + ", diff: " + (parsedTime - trouble));
    

    输出是:

    2016 年 10 月 30 日星期日 02:00:00 CET,差异:0

    但不要解析三个字母的时区缩写

    综上所述,即使有针对秋季重叠情况的解决方法,您在尝试解析时区缩写时仍处于不稳定状态。大多数最常见的都是模棱两可的,你不知道你从解析中得到了什么。在 CET 和 CEST 的情况下,它们是许多欧洲时区的常用缩写,目前在标准时间和夏季时间共享 +01:00 和 +02:00,但历史上每个时区都有自己的偏移量并且很可能自从欧盟决定完全放弃夏季时间后,再次分道扬镳。明年一个时区可能全年使用 CET,另一个时区全年使用 CEST。我上面的代码没有说明了这一点。

    相反,只需从 ZonedDateTime.toString 获取输出,然后使用单参数 ZonedDateTime.parse(CharSequence) 将其解析回来。

    【讨论】:

      【解决方案2】:

      似乎像一个错误。我在 Java 17 中进行了测试,它仍然是相同的行为。我深入研究了解析逻辑,我明白了为什么会发生这种情况。

      首先发生的事情之一是调用TimeZoneNameUtility.getZoneStrings(locale)。这为您提供了一个二维字符串数组

      [
          [
              "Europe/Paris",
              "Central European Standard Time", "CET",
              "Central European Summer Time", "CEST",
              "Central European Time", "CET"
          ],
          // others
      ]
      

      它用它们构建了一个prefix tree。这里的所有项目都映射到第 0 个项目 - "Europe/Paris"。解析时,它一次将前缀树下降一个字符,例如C... E... T...,如果有匹配则返回匹配。由于 CEST 和 CET 映射到同一事物,因此它们实际上只是彼此的别名。

      该字符串后面是passed to ZoneId.of(),这意味着它是否是夏季的事实已被丢弃。

      在 Java 18 中,这段代码似乎确实发生了重大变化,所以也许他们正在解决这个问题。

      【讨论】:

        【解决方案3】:

        对于特定情况是可能的:

        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("EEE MMM dd HH:mm:ss ")
            .appendText(OFFSET_SECONDS, ImmutableMap.of(2L * 60 * 60, "CEST", 1L * 60 * 60, "CET"))
            .appendPattern(" yyyy")
            .toFormatter(Locale.ENGLISH);
        

        这会将确切的偏移量映射到预期的文本。失败的地方是您需要处理多个时区。

        要正确完成这项工作,需要JDK change

        【讨论】:

        • 感谢您提交 JDK 问题。事实上,我还必须支持其他时区(例如 PDT/PST),所以我将坚持使用 SimpleDateFormat 解析包含 z 说明符的模式。
        猜你喜欢
        • 2012-04-07
        • 2011-05-28
        • 2013-10-13
        • 2011-04-29
        • 2012-03-21
        • 2015-08-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多