gpt4 book ai didi

java - java Calendar.toInstant() 和 Instant.atZone(还有 Local/ZonedDateTime.ofInstant())中的不准确性/随机性

转载 作者:行者123 更新时间:2023-12-01 14:31:49 25 4
gpt4 key购买 nike

我正在开发一部分代码,其中我必须使用日历 API 使用现有的 api,而我使用的是全新的 API。在转换中出现了一些奇怪的行为,请看这个例子:

SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");

String date1 = "0000-01-01T00:00:00Z";
Calendar calendar = Calendar.getInstance();
calendar.setTime(df.parse(date1));
Instant instant = calendar.toInstant();
ZonedDateTime zonedDateTime = instant.atZone(calendar.getTimeZone().toZoneId());

System.out.println(calendar.getTime() + " " +calendar.getTimeZone().getDisplayName());
System.out.println(instant.toString());
System.out.println(zonedDateTime.toString());

String date2 = "0000-01-01T00:00:00+01:00";
calendar.setTime(df.parse(date2));
instant = calendar.toInstant();
zonedDateTime = instant.atZone(calendar.getTimeZone().toZoneId());

System.out.println(calendar.getTime() + " " +calendar.getTimeZone().getDisplayName());
System.out.println(instant.toString());
System.out.println(zonedDateTime.toString());

我得到的输出如下:

Thu Jan 01 01:00:00 CET 1 Central European Standard Time
-0001-12-30T00:00:00Z
-0001-12-30T00:09:21+00:09:21[Europe/Paris]
Thu Jan 01 00:00:00 CET 1 Central European Standard Time
-0001-12-29T23:00:00Z
-0001-12-29T23:09:21+00:09:21[Europe/Paris]

因此公历的第一行对于这两种情况都是正确的:

  • 在案例 1 中,我们得到 1 月 1 日凌晨 1:00 的 +01:00 区域
  • 我们在案例 2 中获得 1 月 1 日凌晨 0:00 +01:00 区域

Calendar 转换为 Instant 后,我们已经看到日期有问题,因为我们现在突然:

  • 在案例 1 的 12 月 30 日(48 小时前)
  • 12 月 29 日(72 小时前)在案例 2 中... 还发现在转换过程中还引入了数百毫秒的小随机误差,您在这里看不到

现在,当我们将 next 从 Instant 转换为 ZonedDateTime 时,我们现在就是

  • 9 分 21 秒后,因为欧洲/巴黎时区传递给 instant.atZone() 导致奇怪的时区 +00:09:21

我对它进行了更多测试,一般来说,CalendarInstant 之间的转换对于 1583 年之前的日期变得非常不可靠,而 Instant由于 1911 年之前的日期存在时区问题,Local/ZonedDateTime 变得不可靠。

现在我知道几乎没有人将时间存储/转换为 1911 年之前的日期(但我仍然可以想象这样的用例),但是嘿!让我们看看克里斯托弗·哥伦布是什么时候出发去发现美洲的!

1492-08-03
1492-08-11T23:00:00Z
1492-08-11

因此,我还发现,对于 1911 年之前的早年相同的 ISO 日期,从两个 api 获取 epoch millis 会产生不同的结果(问题似乎出在Calendar 实现中):

System.out.println(Instant.parse("1911-01-01T00:00:00Z").toEpochMilli());
calendar.setTime(df.parse("1911-01-01T00:00:00Z"));
System.out.println(calendar.getTimeInMillis());

正确的转换方式是什么才能“正常工作”(tm)?

注意:到目前为止,我认为最安全的方法是先转换为 ISO 日期字符串。有没有更好的解决方案?

最佳答案

您显示的所有计算都是完全正确的,因为它们保留了以 UTC 表示的瞬间/时刻并使用了公历日期。然而:

期望时区在 1900 年左右的历史引入之前的任何时间都适用是一种非历史的胡说八道。鉴于此,我们不应该尝试比较历史日期的日期和时间表示,而应该只比较瞬间。

在这种情况下,我们甚至应该走得更远更好将自己限制在日期精度而不是日期时间精度。那就是我们最好只讨论日期转换。没有人以毫秒甚至更精确的精度记录历史日期,因为不存在这样精确的时钟。

时区的另一个技术方面是旧的 Java 时区(使用 java.util.TimeZone)和新类 ZoneId 确实使用不同的规则集在您的示例中的 1911 年之前的几年,因为由 IANA(作为 Java 区域的基础)管理的原始 tz-data-repository 中所谓的 LMT 行的处理方式不同。据我所知,来自 Oracle 的 Xueming Chen had once mentioned the year 1900 as cutting date对于旧的 Java 区域,何时应用 LMT(任意选择的城市的本地平均时间)以及何时应用正常的区域时间。欧洲/巴黎等一些区域甚至在 1900 年之后才引入,因此它们仍然显示 LMT 时间。这解释了像 9 分 21 秒这样的差异。详细地说,actual rules for Paris是:

Zone    Europe/Paris    0:09:21 -   LMT 1891 Mar 16
0:09:21 - PMT 1911 Mar 11 # Paris Mean Time
# Shanks & Pottenger give 1940 Jun 14 0:00; go with Excoffier and Le Corre.
0:00 France WE%sT 1940 Jun 14 23:00
# Le Corre says Paris stuck with occupied-France time after the liberation;
# go with Shanks & Pottenger.
1:00 C-Eur CE%sT 1944 Aug 25
0:00 France WE%sT 1945 Sep 16 3:00
1:00 France CE%sT 1977
1:00 EU CE%sT

这意味着,java.util.TimeZone 对 1911 年之后的所有历史日期应用第一条非 LMT 线(PMT 线由于偏移量不变而被忽略),因为切割年份为 1900 年。但是 ZoneId 使用 LMT 行 - 如您的示例所示。

我们看到,不同的处理是一种任意选择的技术实现策略,用于处理分区日期时间,即使是在人们拥有精确时钟并且根本不知道时区概念之前的历史时刻。

那是关于时区的事情,现在我们来谈谈日期转换的处理。

新的 java.time-package 只使用 proleptic gregorian dates,即使是在没有人知道 gregorian date 是什么的日期。这样的日期是在 1582 年由 Pope Gregor 首次引入的。他砍掉了 10 天(因此 1582 年的 10 月 5 日到 10 月 14 日的日期不存在)并引入了一个不同于旧儒略历规则的闰年规则。实际上,旧类 java.util.Calendar 将 1582-10-15 之前的所有日期作为 Julian 日期处理,这解释了您观察到的其他差异。如果将 10 天的截断与不同的闰年规则结合起来,那么您将看到 0000 年儒略历和公历之间的两天差异。

您似乎希望使用 java.time-API 中的新类重现有关旧日期的旧行为。嗯,这是不可能的。不是新 API 中的错误,而只是限制为公历日期而不支持 Julian 日历日期的设计决定。

尽管如此,如果您坚持要查看 1583 年之前的 Julian 日期并避免使用 java.util.CalendarSimpleDateFormat 等旧类,那么您可以使用我的库 Time4J它具有到 java.time-API 的转换方法,并为您的哥伦布示例(西类牙出发日)使用这样的代码:

ChronoHistory history =
ChronoHistory.ofFirstGregorianReform(); // the same behaviour like in old Java
ChronoFormatter f =
ChronoFormatter
.ofPattern("yyyy-MM-dd", PatternType.CLDR, Locale.ROOT, HistoricCalendar.family())
.withDefault(HistoricCalendar.ERA, HistoricEra.AD)
.with(history);
HistoricCalendar expected =
HistoricCalendar.of(history, HistoricEra.AD, 1492, 8, 3);
assertThat(
f.parse("1492-08-03"), // date of departure of Columbus to America
is(expected));
assertThat(
expected.transform(PlainDate.axis()), // transformation to gregorian
is(PlainDate.of(1492, 8, 12)));
// conversion to java.time-API
LocalDate threetenAlwaysGregorian = PlainDate.of(1492, 8, 12).toTemporalAccessor();

您的转换导致 1492-08-11(一天前)的事实只是由于时区效应。为避免此类情况,您确实应该限制日期精度,或者如果不可能,您至少应该在中午而不是午夜转换日期。

关于java - java Calendar.toInstant() 和 Instant.atZone(还有 Local/ZonedDateTime.ofInstant())中的不准确性/随机性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62712678/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com