3

我正在开发一部分代码,我必须使用日历 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]

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

  • 在 case1 中,我们在 +01:00 区域获得 1 月 1 日凌晨 1:00
  • 在 case2 中,我们在 +01:00 区域获得 1 月 1 日上午 0:00

在从Calendarto转换之后,Instant我们已经看到了日期的问题,因为我们现在突然:

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

现在,当我们转换 next from 时InstantZonedDateTime我们现在

  • 9 分 21 秒后,因为时区 Europe/Paris 传递到instant.atZone()导致 +00:09:21 的奇怪时区

我对它进行了更多测试,并且通常在 1583 年之前的日期之间进行转换Calendar并且Instant变得非常不可靠,而由于 1911 年之前的日期的时区问题,Instantto变得不可靠。Local/ZonedDateTime

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

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

结果,我还发现,在 1911 年之前的早期,从两个 api 获取 epoch millis 会为相同的 ISO 日期产生不同的结果(问题似乎出在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 日期字符串。有没有更好的解决方案?

4

1 回答 1

1

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

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

在这种情况下,我们甚至应该更进一步,更好地将自己限制在日期精度而不是 date-time-precision上。那就是我们最好只讨论日期转换。没有人以毫秒甚至更高的精度记录历史日期,因为不存在这样精确的时钟。

时区的另一个技术方面是,旧 Java 时区(使用java.util.TimeZone)和新类ZoneId在您的示例中确实在 1911 年之前使用了不同的规则集,因为原始 tz-data-repository 中所谓的 LMT 行管理IANA(作为 Java 区域的基础)的处理方式不同。据我所知,Oracle的陈学明曾经提到 1900 年作为旧 Java 区域何时应用 LMT(任意选择城市的本地平均时间)和何时应用正常分区时间的截止日期。欧洲/巴黎等一些区域甚至在 1900 年之后引入,因此它们仍然显示 LMT 时间。这解释了像 9 分 21 秒这样的差异。详细来说,巴黎的实际规则是:

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由于切割年份 1900,在 1911 年之后对所有历史日期应用第一条非 LMT 线(PMT 线由于偏移量不变而被忽略)。但ZoneId使用 LMT 线 - 如您的示例中所示。

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

那是关于时区的东西,现在让我们谈谈日期转换的处理。

新的java.time-package 仅使用预测的公历日期,即使对于没有人知道公历日期是什么的日期也是如此。此类日期由教皇格里高尔在 1582 年首次引入。他切断了 10 天(因此 1582 年 10 月 5 日至 10 月 14 日的日期不存在)并引入了与旧儒略历规则不同的闰年规则。实际上,旧类java.util.Calendar将 1582-10-15 之前的所有日期都处理为 Julian 日期,这解释了您观察到的其他差异。如果您将 10 天的删减与不同的闰年规则结合起来,那么您将看到 0000 年儒略历和公历之间的两天差异。

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

不过,如果您坚持要查看 1583 年之前的朱利安日期并避免使用旧类,java.util.Calendar或者SimpleDateFormat然后您可以使用我的库 Time4J,它具有到 -API 的转换方法,java.time并为您的哥伦布示例使用这样的代码(西班牙出发日):

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(提前一天)这一事实只是由于时区效应。为了避免这种情况,您应该真正限制日期精度,或者如果不可能,您至少应该在中午而不是午夜转换日期。

于 2020-07-04T07:36:07.177 回答