Java DateTime与1582历法问题

凯撒历和格里高利历间转换的隐性问题

Posted by Chris on March 8, 2017

事情经过

今天下午在研究mybatis对jsr310的支持, 跟往常一样我准备了一个MySQL测试用的数据表, 里面有些DATETIME列, 默认值是’0000-01-01 00:00:00’, 映射Java类中的create_time等这样的java.time.OffsetDateTime属性.

当我测试的时候我就整个蒙圈了, 某些字段插入我是用OffsetDateTime.now()指定, 这些数据取出来的时候完整无误, 存的时间多少他取出来就是多少, 如2017-03-08T16:14:52+08:00. 但是当我不设置该值, 选用数据库的默认值的时候, 一返回我就傻了, 0001-01-01T00:05:43+08:05:43, 这个偏移值真的是闻所未闻… 考虑过Docker MySQL镜像时间的同步问题, 甚至还开始怀疑是不是JDK8有BUG了.

于是乎丫的我就直接开始调试, 直到抓到一条线索, MyBatis从数据库取出至封装至Java Bean的途中, 调用了一个java.sql.Timestamp.from(Instant)函数, 就是这个函数调用出了问题. 测试之后真的是让我大开眼界.

System.out.println(Timestamp.from(OffsetDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneOffset.ofHours(8)).toInstant()));
System.out.println(Timestamp.from(OffsetDateTime.of(1582, 10, 14, 23, 59, 59, 999999999, ZoneOffset.ofHours(8)).toInstant()));
System.out.println(Timestamp.from(OffsetDateTime.of(1582, 10, 15, 0, 0, 0, 0, ZoneOffset.ofHours(8)).toInstant()));

跑出来的结果如下

0001-01-03 00:00:00.0
1582-10-04 23:59:59.999999999
1582-10-15 00:00:00.0

在1582年10月15号之前的日期是有问题的, 非常奇特, 于是乎谷歌一番, 让我找到了答案

原因

java.time包中,像所有主要的时间/日期类一样,LocalDate是固定于单个历法系统的:由ISO-8601标准定义。

ISO-8601历法系统是事实上的世界民用历法系统,也就是公历。平年有365天,闰年是366天。闰年的定义是:非世纪年,能被4整除;世纪年能被400整除。为了计算的一致性,公元1年的前一年被当做公元0年,以此类推。

采用这套历法,第一个影响就是,ISO-8601的日期不必跟GregorianCalendar一致。在GregorianCalendar中,凯撒历格里高利历之间有一个转换日,一般默认在1582年10月15日。那天之前,用凯撒历:每4年一个闰年,没有例外。那天之后,用格里高利历,也就是公历,拥有稍微复杂点的闰年计算方式。

既然凯撒历和格里高利历之间的转换是个历史事实,那为什么新的java.time开发包不参照它呢?原因就是,现在使用历史日期的大部分Java应用程序,都是不正确的,继续下去,是个错误。这是为什么呢?当年,罗马的梵蒂冈,把历法从凯撒历改换成格里高利历的时候,世界上大部分其他地区并没有更换历法。比如大英帝国,包括早期的美国,直到大约200后的1752年9月14日才换历法,沙俄直到1918年2月14日,而瑞典的历法转换更是一团糟。因此,实际上,对1918之前的日期,解释是相当多的;仅相信拥有单一转换日的GregorianCalendar,是不靠谱的。所以LocalDate中没有这种转换,就是一个合理的选择了。应用程序需要额外的上下文信息,才能在凯撒历和格里高利历间,精确的解释特定的历史日期。

小结

浪费我两个多小时, 给我好好上了一堂历史课.