我们如何开发编码实践来防止闰年 bug?

微软上周刚刚宣布,在计算日期(闰年) 导致了 Windows Azure 的大面积停机时出现了一个软件错误。

在闰年,围绕 DateTime.Now.AddYears(1)的判断真的只是一个简单的错误吗?

什么样的编码实践可以阻止这种情况的发生?

剪辑 正如 dcraw 指出的,在闰年的 DateTime.Now.AddYears(1)确实返回了正确的日期。NET.所以它不是一个框架错误,但显然是 Date 计算中的一个错误。

3282 次浏览

无耻插头:

使用更好的日期和时间 API

内置的。NET 日期和时间库非常难以正确使用。他们的 让你做一切你需要的,但你不能自己清楚地通过 快递类型系统。DateTime一团糟DateTimeOffset可能会诱使你认为你实际上保存的时区信息,当你不是,和 TimeZoneInfo不会强迫你去想你应该考虑的一切。

这些都不能很好地表达“只是一天中的某个时间”或“只是一个日期”,也不能明确区分“当地时间”和“特定时区的时间”。如果你想使用公历以外的日历,你需要通过 Calendar类的全部时间。

所有这些都是我为什么要构建 野田时光的原因——一个建立在 乔达时间“引擎”端口上的替代日期和时间库,但是在它的顶部有一个新的(更精简的) API。

一些你可能想要思考的问题,如果你没有意识到的话,很容易被忽略:

  • 将本地日期/时间映射到特定时区的日期/时间并不像您想象的那么简单。由于夏令时过渡,特定的本地日期/时间可能出现一次、两次(模糊)或零次(跳过)
  • 时区在历史上有所不同——坦率地说,超过 TimeZoneInfo通常愿意透露的时区。(它不支持“标准时间”观念随时间变化的时区,也不支持永久夏时制的时区。)
  • 即使有时区信息数据库时区 ID 也不一定稳定。(CLDR 解决了这个问题; 我希望最终能在 Noda Time 中支持这一点。)
  • 日期和时间的文本表示是一场噩梦,不仅仅是在排序方面,还包括日期分隔符、时间分隔符和一些奇怪的东西,比如属性月份名称
  • 一天的开始并不总是在午夜——例如,在巴西,春季夏令时的过渡把挂钟从晚上11:59:59移动到凌晨1点
  • 在某些情况下(好吧,据我所知) ,一个时区可以强迫跳过一整天——2011年12月30日并没有发生在萨摩亚!我怀疑大多数开发者可以忽略这一点,但是..。
  • 如果你打算使用公历以外的日历,一定要小心,确保你真的知道你期望它如何运作。

至于具体的发展做法:

  • 想想你到底想表达什么。我期望 Noda Time 的核心好处是迫使开发人员在各种不同类型之间进行选择,以表示他们的数据。只要做到这一点,其他的一切都会变得简单。
  • 单元测试您能想到的所有东西。当然,这将取决于系统的具体功能,但是 尤其是考虑的是不同的时区,夏令时过渡期间发生的情况,当然还有闰年。
  • 我建议注入一个“类似时钟的接口”——一个用于告知当前时间的服务——而不是显式地调用 DateTime.NowDateTime.UtcNow; 这样做更容易(可行!)到单元测试
  • 如果使用“ now”执行多个操作,那么获取日期/时间 一次并记住它,而不是重复请求“ now”——否则在调用之间,值可能会以不幸的方式发生变化。
  • 如果我想知道“‘从现在开始的两个星期’到底什么时候在我的本地时区发生?”然后我需要存储 本地日期/时间以及时区。

值得注意的是,这个错误可能不是由于你发布的一句话:

DateTime.Now.AddYears(1)

不会创建无效日期。如果运行:

(new DateTime(2012, 2, 29)).AddYears(1)

2013年2月28日。我不知道 Azure 的客户代理是用什么写的,但肯定是另一个电话打不通。这样做不太好。NET 会是:

new DateTime(today.Year + 1, today.Month, today.Day)

如果 today是闰日,则抛出一个异常。然而,微软关于 Azure 问题的博客说,他们创建了一个无效的日期2013年2月29日,我不确定是否可能与 DateTime在。NET.

我并不是说 DateTimeDateTimeOffset不容易出错,只是我认为它们不会引起这个特殊的问题。

我们如何开发编码实践来防止闰年 bug? 什么样的编码实践可以阻止这种情况的发生?

John 提到的单元测试特定日期是一种代码实践,它将有助于我定义的“手动集成测试”

更改您的开发/测试平台服务器上的时钟,并观察当时间过去时会发生什么。

不要纠结于这是否是一种“编程练习”——显然你不能对日历上的每一天都这样做——选择你关心的日期,无论是2月29日,月底日期或夏时制转换日期。