解析 RFC 3339 日期时间时不考虑区域调整

Zone adjustment not taken into account when parsing RFC 3339 date time

(迁移自 CodeReview

我正在试验并试图更好地理解 Java 时间。我的代码没有按预期工作,我想问问原因,可能是因为我对 JSR-310 时区处理有误解。

我有一个要用 DateTimeFormatter

解析的时间戳字符串
String text = "2019-08-28T10:39:57+02:00";
DateTimeFormatter ISO_RFC_3339_PARSER = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("Europe/Paris"));

以上值是当服务器的本地时间为中欧10:39上午时生成的。由于夏令时的原因,UTC 时间是 8:39 早上。您可以自行计算找出您所在区域的时间。

当计算机的时区与时间戳中显示的时区相同时,以下测试有效(+2)

@Test
public void testTimestamp()
{
    LocalDateTime time = ZonedDateTime.from(ISO_RFC_3339_PARSER.parse(text)).toLocalDateTime();

    errorCollector.checkThat(time.getYear(), is(2019));
    errorCollector.checkThat(time.getMonthValue(), is(8));
    errorCollector.checkThat(time.getDayOfMonth(), is(28));
    errorCollector.checkThat(time.getHour(), is(10));
    errorCollector.checkThat(time.getMinute(), is(39));
    errorCollector.checkThat(time.getSecond(), is(57));

}

我已经尝试 修补 输入字符串以更好地理解 Java 时间如何调整时区。但是结果却大错特错!

我试图反复更改输入字符串中的时区,因此测试应该会失败。例如,如果我将字符串更改为 2019-08-28T10:39:57+08:00,则表示现在是巴黎凌晨 2 点。但是上面的测试代码在检查一天中的小时时继续通过。 IE。结果 LocalDateTime 当天的时间仍然是上午 10 点。

问题

为什么我的代码仍然返回 10 作为 local 一天中的时间,而不管我不断更改源字符串中的时区这一事实?

那么解析 RFC 3339 字符串(表示嵌入偏移量中的一个瞬间)并将其转换为可能的区域调整的 LocalDateTime 对象的正确方法是什么?假设机器在CE[S]T时区运行.

环境

我需要将一个时间戳与计算机时间进行比较,看看它是不是太旧了。这意味着如果在欧洲评估美国时间,它可能不会“太旧”。

    String text = "2019-08-28T10:39:57+02:00";
    OffsetDateTime time = OffsetDateTime.parse(text);
    System.out.println("Hour of day is " + time.getHour());

输出是(如您所料):

Hour of day is 10

尝试不同的偏移量:

    String text = "2019-08-28T10:39:57+08:00";

Hour of day is 10

所以还是一样,仍然是字符串中给定的小时。这就是 10 的来源。我从你的代码中选择了一个稍微简单的代码示例,但它演示的行为与你的代码相同,没有区别,也足以解释。

需要注意的一点是代码不依赖于 JVM 的时区设置(您报告为 Europe/Paris)。那挺好的。这意味着如果我们相信代码是正确的,我们也可以相信它 运行 在所有时区的所有 JVM 上都是正确的。

在您的代码中,您通过调用 toLocalDateTime() 转换为 LocalDateTime。由于 OffsetDateTimeZonedDateTime 很好地实现了目的,因此这种转换通常没有任何充分的理由。转换的作用是保留 OffsetDateTimeZonedDateTime 中的日期和小时(挂钟时间)并丢弃偏移量或时区信息。因此,如果一天中的小时在转换前为 10,则转换后也将为 10。造成您混淆的一个可能原因是您是否期望转换为 您的 当地时间 (Europe/Paris)。不执行此类转换。 java.time names 中的 Local 表示:没有时区或偏移量。有人认为使用 Local 这个词来表达这个意思是误导(它是历史性的,因为它是从 Joda-Time 接管的,运行 是 java.time).

Then what is the correct way to parse a RFC 3339 string (which represents an instant in the embedded offset) and convert it into a LocalDateTime object with regards of possible zone adjustments? Assume the machine is running in CE[S]T time zone.

解析已经为您正常工作。您可能不需要转换。你可以直接比较。 OffsetDateTimeZonedDateTimeisBeforeisEqualisAfter 方法在比较时会考虑不同的偏移量或时区,并会为您提供所需的结果.如果你确实想转换:

    String text = "2019-08-28T10:39:57+08:00";
    OffsetDateTime time = OffsetDateTime.parse(text);
    System.out.println("Hour of day is " + time.getHour());

    ZoneId myTimeZone = ZoneId.of("Europe/Paris");
    ZonedDateTime timeInFrance = time.atZoneSameInstant(myTimeZone);
    System.out.println("Hour of day in France is " + timeInFrance.getHour());
Hour of day is 10
Hour of day in France is 4

同样,您可以转换为 LocalDateTime 并保留 4 小时,但我不明白您为什么要这样做。

关于我对您的代码的简化:您的 RFC 3339 字符串包含与 UTC (+02:00) 的偏移量,而不是时区(如 Europe/Paris 或 Europe/Rome)。所以使用 ZonedDateTime 来解析它是矫枉过正的; OffsetDateTime 更合适。字符串格式符合 ISO 8601。java.time 类 无需任何显式格式化程序即可开箱即用地解析最常见的 ISO 8601 格式变体,因此我们在这里不需要格式化程序。顺便说一句,您分配给格式化程序的区域没有任何区别,因为使用了字符串中的偏移量。

Link: Wikipedia article: ISO 8601

ZonedDateTime 得到 LocalDateTime 只是削减了时区。它的价值无关紧要。您需要做的是将您的 ZonedDateTime 从 +8 区转换为 +2 区,这将相应地更改时间。然后你可以从你修改的 ZonedDateTime 中得到 LocalDateTime 以获得想要的效果。这是一个演示它的例子:

private static void testDateParsing() {
    ZonedDateTime zdt = ZonedDateTime.parse("2019-08-28T10:39:57+08:00", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZZZZZ"));
    ZonedDateTime modifiedZdt = zdt.withZoneSameInstant(ZoneId.systemDefault());
    System.out.println(zdt);
    System.out.println(modifiedZdt);
    System.out.println(LocalDateTime.from(zdt));
    System.out.println(LocalDateTime.from(modifiedZdt));
}

输出为:

2019-08-28T10:39:57+08:00
2019-08-28T05:39:57+03:00[Asia/Jerusalem]
2019-08-28T10:39:57
2019-08-28T05:39:57

请注意,我的本地时区是 +03,而不是你的 +02。