为什么2020年3月30日和3月1日之间的天数错误地显示为28天而不是29天?

129
TimeUnit.DAYS.convert(
   Math.abs(
      new SimpleDateFormat("dd-MM-yyyy HH:mm:ss").parse("30-03-2020 00:00:00").getTime() - 
      new SimpleDateFormat("dd-MM-yyyy HH:mm:ss").parse("1-03-2020 00:00:00").getTime()
   ),
   TimeUnit.MILLISECONDS)

结果为28,但应该是29。

时区/位置是否可能是问题所在?


17
注意:请不要再使用已过时的SimpleDateFormat,而应该使用java.time包中的工具。对于SimpleDateFormat,请使用DateTimeFormatter代替。如果使用Java 7,请参考下面Andy Turner的评论。 - MC Emperor
28
不要在时间上进行数学运算。使用适当的时间库(例如 java.time [尽管我注意到您使用的是 Java 7],ThreeTenBp,Joda)。 - Andy Turner
14
我很想参加那个会议,听到有人说:“好的,既然我们已经基本搞清楚了时区问题,那么让我们进入100%的工作模式,实施这个叫做夏令时的东西吧,这个想法是昨晚在我的酸性旅程中梦到的。” - MonkeyZeus
5
TimeUnit 不涉及夏令时的问题。正如 javadoc 所说:“纳秒被定义为微秒的千分之一,微秒被定义为毫秒的千分之一,毫秒被定义为秒的千分之一,分钟被定义为六十秒,小时被定义为六十分钟, 一天被定义为二十四小时。”因为夏令时导致一年中有2天不是精确的24小时,所以在涉及夏令时时,TimeUnit 的计算会出现误差。 - Andreas
1
这个问题只会出现在使用夏令时的时区的电脑上。在不使用夏令时的时区,它会正确地显示天数(29)! - Gopinath
显示剩余3条评论
2个回答

211

问题在于,由于夏令时变更(于2020年3月8日星期日),这两个日期之间有28天23小时TimeUnit.DAYS.convert(...)将结果截断为28天。

要查看问题(我在美国东部时区):

SimpleDateFormat fmt = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
long diff = fmt.parse("30-03-2020 00:00:00").getTime() -
            fmt.parse("1-03-2020 00:00:00").getTime();

System.out.println(diff);
System.out.println("Days: " + TimeUnit.DAYS.convert(Math.abs(diff), TimeUnit.MILLISECONDS));
System.out.println("Hours: " + TimeUnit.HOURS.convert(Math.abs(diff), TimeUnit.MILLISECONDS));
System.out.println("Days: " + TimeUnit.HOURS.convert(Math.abs(diff), TimeUnit.MILLISECONDS) / 24.0);

输出

2502000000
Days: 28
Hours: 695
Days: 28.958333333333332

要解决问题,请使用一个不包含夏令时的时区,例如UTC

SimpleDateFormat fmt = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
long diff = fmt.parse("30-03-2020 00:00:00").getTime() -
            fmt.parse("1-03-2020 00:00:00").getTime();

输出

2505600000
Days: 29
Hours: 696
Days: 29.0

126
不要使用数学计算时间来进行修复。使用适当的日期/时间库。 - Andy Turner
63
使用内置的Java 7 API可以解决这个问题。因为它是可以修复的,所以展示如何修复是一个有效的答案。强迫某人包括一个完整的库(Joda-Time、ThreeTen等)来执行这个计算会过于繁琐。当然,推荐使用库,但不是必须的。 - Andreas
40
我尊重地不同意。如果你想进行计算,就要做好它;为了正确地完成它,需要付出代价。 - Andy Turner
16
我的意见是,在Java 7中不使用任何外部库的“正确”方法是使用GregorianCalendar对象,每次添加一天,直到达到结束日期。但我宁愿花费高价避免这样做。而且添加一个已经包含在Java 8、9、10、11、12、13等版本中的库的倒退版本并不需要付出高昂的代价。相反,下次需要进行日期或时间处理时,就已经节省了很多时间。 - Ole V.V.
27
即使在UTC,六月的最后一天偶尔也会短了一秒,这没有任何真正的警告或可预测性。请始终使用日期时间库。 - Affe
显示剩余11条评论

41
这个问题的原因已经在Andreas's answer中提到了。
问题是你想要计算什么。你指出实际差异应该是29而不是28,并询问“位置/区域时间可能有问题”,揭示了你实际想要计算什么。显然,你想摆脱任何时区差异。
我假设你只想计算天数,不考虑时间和时区。
Java 8
下面是一个示例,展示如何正确计算天数,使用的是一个表示没有时间和时区的日期的类 - LocalDate。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d-MM-yyyy HH:mm:ss");
LocalDate start = LocalDate.parse("1-03-2020 00:00:00", formatter);
LocalDate end = LocalDate.parse("30-03-2020 00:00:00", formatter);

long daysBetween = ChronoUnit.DAYS.between(start, end);

请注意,ChronoUnitDateTimeFormatterLocalDate至少需要Java 8,而根据标签,这对您不可用。然而,对于未来的读者可能是可用的。
正如Ole V.V.所提到的,还有ThreeTen Backport,它将Java 8日期和时间API功能回溯到Java 6和7。

2
@OleV.V. 我知道有ThreeTen,一些用户可能已经提到过几次了。(我正要链接到一个SEDE查询,返回所有包含文本“ ThreeTen”的用户5772882的帖子和评论;-),但不幸的是在写作期间它已下线。)我会更新这篇文章的。 - MC Emperor
2
@OleVV 这一点并不是坏事。我认为大多数人只是对 java.time 不了解,因为在学校里他们仍然使用旧的类。但是 Java 8 的日期和时间 API 设计非常好 - 不使用它将是一种损失。 - MC Emperor

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