为什么将公元1582年之前的Java日期转换为带有Instant的LocalDate会得到不同的日期?

11

考虑以下代码:

Date date = new SimpleDateFormat("MMddyyyy").parse("01011500");

LocalDate localDateRight = LocalDate.parse(formatter.format(date), dateFormatter);
LocalDate localDateWrong = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()).toLocalDate();

System.out.println(date);           // Wed Jan 01 00:00:00 EST 1500
System.out.println(localDateRight); // 1500-01-01
System.out.println(localDateWrong); // 1500-01-10

我知道1582年是儒略历和格里高利历的分界线,但我不知道为什么会这样,也不知道如何进行调整。

到目前为止,我已经弄清楚了以下内容:

  • 日期对象具有设置为JulianCalendarBaseCalender
  • date.toInstant()只返回Instant.ofEpochMilli(getTime())
  • date.getTime()返回-14830974000000
  • -14830974000000是格里高利历的周三,1500年1月10日05:00:00 GMT(Gregorian)

因此,看起来要么getTime()返回的毫秒数是错误的(不太可能),要么与我的预期不同,我需要考虑差异。


请注意,在切换期间会有几个“不计算”的日期,因为日历向前跳了。因此,任何处于“虚假日期”范围内的日期,如果没有被标记为明显的错误,则将被任意解析为“真实”日期。 - Hot Licks
谢谢提醒!这种情况特别发生在格里高利日历实行之前的日期。 - Rick Hanlon II
“公历截止日期”并没有明确定义。新日历的采用大约需要500年时间。不同的日历实现可能使用不同的日期,因为特定日期相当随意。 - Hot Licks
1
在Date类中已经很好地定义了。来自于gregorianCutoff字段的注释:“默认值为 1582 年 10 月 15 日(公历)00:00:00 UTC 或 -12219292800000L。对于此值,1582 年 10 月 4 日(儒略历)之后是 1582 年 10 月 15 日(公历)。” - Rick Hanlon II
1个回答

20

LocalDate仅处理普通公历日历。从其javadoc

ISO-8601日历系统是当今世界上使用的现代公民日历系统。它相当于普通公历日历系统,在其中适用于所有时间的闰年规则。对于今天编写的大多数应用程序,ISO-8601规则完全合适。但是,任何利用历史日期并要求它们准确的应用程序都会发现ISO-8601方法不适用。

相比之下,旧的java.util.GregorianCalendar类(也间接用于java.util.Date的toString()-输出)使用可配置的格里高利分界线,默认为1582-10-15,作为儒略和格里高利日历规则之间的分离日期。

因此,LocalDate不能用于任何类型的历史日期。

请注意,即使是java.util.GregorianCalendar在正确配置区域依赖的截止日期时也经常失败。例如,在1752年之前,英国在3月25日开始新的一年。而且许多国家还有更多的历史偏差。在欧洲以外,甚至在引入格里高利历之前,朱利安历也无法使用(或者只能从殖民主义者的角度最好地使用)。 更新:由于评论中的问题: 为了解释值-14830974000000,让我们考虑以下代码及其输出:
SimpleDateFormat format = new SimpleDateFormat("MMddyyyy", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("America/New_York"));
Date d = format.parse("01011500");

long t1500 = d.getTime();
long tCutOver = format.parse("10151582").getTime(); 
System.out.println(t1500); // -14830974000000
System.out.println(tCutOver); // default gregorian cut off day in "epoch millis"
System.out.println((tCutOver - t1500) / 1000); // output: 2611699200 = 30228 * 86400

请注意,你之前评论中提到的值-12219292800000L由于America/New_YorkUTC之间的时区偏差不同,与tCutOver相差5个小时。因此,在EST时区(美国/纽约),我们有确切的30228天差异。对于所讨论的时间跨度,我们应用儒略历规则,即每四年为闰年。
在1500年至1582年期间,我们有82 * 365天+21个闰日。然后我们还需要在1582-01-01和1582-10-01之间添加273天,最后再加上4天直到切换(记住10月4日后面是10月15日)。总共:82 * 365 + 21 + 273 + 4 = 30228(需要证明的内容)。
请解释一下,为什么你期望的时间值与 -14830974000000 毫秒不同。对我来说看起来是正确的,因为它处理了您系统的时区偏移、1582年之前的儒略历规则以及从1582年10月4日跳转到切换日期1582-10-15。所以对于我来说,你的问题“如何告诉日期对象将毫秒返回到正确的公历日期?”已经得到了回答——无需更正。请记住,这些复杂的内容在生产中使用了相当长的时间,并且可以预期在这么多年后能够正确地工作。

如果你真的想要使用JSR-310来处理这些内容,我要重申,它没有支持公历改革日期的功能。最好的方法是你自己做一个解决方案。

例如,你可以考虑使用外部库Threeten-Extra,它自0.9版本开始包含一个追溯儒略历的日历。但是你仍然需要努力处理旧儒略历和新公历之间的转换。(并且不要指望这样的库能够处理真实的历史日期,因为还有许多其他原因,比如新年的开始等等。)

2017年更新: 另一个更强大的选项是使用Time4J库中的HistoricCalendar,它可以处理比仅仅儒略/格里历转换更多的内容。


这很有道理。那么为什么Date对象返回-14830974000000ms,这是错误的格里高利历毫秒数,或者为什么asInstant函数没有考虑到Date对象的基准日历?对我来说更重要的是,我如何告诉日期对象将毫秒数返回到正确的格里高利日期? - Rick Hanlon II
@RickHanlonII 请看我的更新答案。顺便问一下,您管理的是什么类型的数据?历史事件的日期吗?我只是好奇。 - Meno Hochschild
关于 date.toInstant(),“自UNIX纪元以来的毫秒数”这个概念与日历规则无关。撇开在此处使用的固定转换因子(一天= 86400 * 1000毫秒),您在朱利安和格里高利日历规则之间没有任何变化,并且在这样的机器尺度上也没有跳跃切换。 - Meno Hochschild

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接