有哪些闰年错误的例子?

20
一种闰年错误是代码缺陷,在闰年上下文中执行时会产生问题和意外结果,通常在普通公历系统内。
最后一个闰年是2016年。下一个闰年是2020年和2024年。
闰年有两个独特的属性:
- 闰年有2月29日,而平年没有。 - 闰年总共有366天,而平年只有365天。
本篇文章旨在帮助其他人了解闰年错误的性质,以及它们在各种语言中的表现形式和如何进行纠正。
闰年错误通常分为两类影响:
- 类别1:导致错误条件,例如异常、错误返回代码、未初始化变量或无限循环 - 类别2:导致不正确的数据,例如范围查询或聚合中的偏移问题
请在每个答案中指出编程语言和/或平台,以及上述定义的影响类别。(请遵循现有答案使用的模板。)
请针对每种语言和缺陷类型创建一个单独的答案,并投票支持你喜欢的答案,特别是那些你个人遇到过的(尽可能留下评论和轶事)。
6个回答

8

.NET / C# - 从日期部分构建

影响类别1

有瑕疵的代码

DateTime dt = DateTime.Now;
DateTime result = new DateTime(dt.Year + 1, dt.Month, dt.Day);

这段代码在 dt 变成2月29日之前是正常工作的。然而,一旦 dt 变成了公历闰年的2月29日,代码将尝试去创建一个不存在的日期。这时,DateTime 构造函数会抛出 ArgumentOutOfRangeException 异常。

类似的情况还包括任何形式的 DateTimeDateTimeOffset 构造函数,只要传入的年、月、日参数来自不同的来源或无视整个日期的有效性而进行操作,都可能导致问题。

修改后的代码

DateTime dt = DateTime.Now;
DateTime result = dt.AddYears(1);

常见变化-生日(和其他纪念日)

一种变化是在不考虑闰年生日(即2月29日出生的人)的情况下确定用户当前的生日。它还适用于其他类型的周年纪念日,例如入职日期、服务日期、结算日期等。

存在问题的代码

DateTime birthdayThisYear = new DateTime(DateTime.Now.Year, dob.Month, dob.Day);

这种方法需要调整,例如以下代码对于平年使用2月28日。(尽管在某些情况下可能更喜欢使用3月1日。)

修正后的代码

int year = DateTime.Now.Year;
int month = dob.Month;
int day = dob.Day;
if (month == 2 && day == 29 && !DateTime.IsLeapYear(year))
    day--;

DateTime birthdayThisYear = new DateTime(year, month, day);

修正代码(另一种实现)

DateTime birthdayThisYear = dob.AddYears(DateTime.Now.Year - dob.Year);

3

Win32 / C++ - SYSTEMTIME结构体操作

影响类别 1

有缺陷的代码

SYSTEMTIME st;
FILETIME ft;

GetSystemTime(&st);
st.wYear++;

SystemTimeToFileTime(&st, &ft);

这段代码在2月29日之前可以正常工作。但是,在这一天,它将尝试创建一个不存在的普通年份的2月29日。 将其传递给任何接受SYSTEMTIME结构的函数可能会失败。

例如,此处显示的SystemTimeToFileTime调用将返回错误代码。由于未检查该返回值(这是非常常见的),因此导致ft未被初始化。

修正的代码

SYSTEMTIME st;
FILETIME ft;

GetSystemTime(&st);
st.wYear++;

bool isLeapYear = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
st.wDay = st.wMonth == 2 && st.wDay == 29 && !isLeapYear ? 28 : st.wDay;

bool ok = SystemTimeToFileTime(&st, &ft);
if (!ok)
{
  // handle error
}

这个修复程序检查一个普通年份的2月29日,并将其更正为2月28日。


3

Python - 替换年份

影响类别 1

存在问题的代码

from datetime import date
today = date.today()
later = today.replace(year = today.year + 1)

这段代码在今天之前都能正常工作,但一旦到了2月29日,它将尝试创建一个不存在的普通年份的2月29日。 date 构造函数将引发一个带有消息 "day is out of range for month"ValueError 异常。

变化:

from datetime import date
today = date.today()
later = date(today.year + 1, today.month, today.day)

纠正后的代码

不使用任何其他库,可以捕获错误:

from datetime import date
today = date.today()
try:
    later = today.replace(year = today.year + 1)
except ValueError:
    later = date(today.year + 1, 2, 28)

然而,通常最好使用诸如dateutil这样的库:

from datetime import date
from dateutil.relativedelta import relativedelta
today = date.today()
later = today + relativedelta(years=1)

1

判断一年是否为闰年

本文例子使用C#,但这个模式适用于所有编程语言。

影响类别2

有缺陷的代码

bool isLeapYear = year % 4 == 0;

这段代码错误地假设闰年恰好每四年出现一次。但我们在商业和计算中最常使用的格里高利历法系统并非如此。

更正后的代码

完整算法(来自维基百科)如下:

如果年份不可被4整除),(它是平年)
否则,如果年份不可被100整除),(它是闰年)
否则,如果年份不可被400整除),(它是平年)
否则(它是闰年)

该算法的一个实现如下:

bool isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);

在许多平台上,此功能已内置。例如,在.Net中,以下内容是首选:
bool isLeapYear = DateTime.IsLeapyear(year);

0

JavaScript - 添加年份

影响类别 2

有缺陷的代码

var dt = new Date();
dt.setFullYear(dt.getFullYear() + 1);

这段代码在dt变成2月29日(例如在2020-02-29)之前可以正常工作。然后它将尝试将年份设置为2021年。由于2021-02-29不存在,Date对象将向前滚动到下一个有效日期,即2020-03-01

在某些情况下,这可能是预期的行为。在其他情况下,相差一天可能没有什么影响(例如过期日期)。

然而,在许多情况下,提前一年的意图是保持大致相同的月份和日期位置。换句话说,如果您从二月底开始(2020-02-29),并提前一年,您可能希望结果也是在二月底(2021-02-28),而不是三月初(2021-03-01)。

更正后的代码

要在JavaScript中添加年份并保留2月底的行为,请使用以下函数。

function addYears(dt, n) {
  var m = dt.getMonth();
  dt.setFullYear(dt.getFullYear() + n);
  if (dt.getMonth() !== m)
    dt.setDate(dt.getDate() - 1);
}

// example usage
addYears(dt, 1);

常见变体 - 不可变形式

有时候我们会有一些不会改变原始对象的代码,例如下面这段:

有缺陷的代码

var dt = new Date();
var result = new Date(dt.getFullYear() + 1, dt.getMonth(), dt.getDate());

保持二月末行为不变的不可变形式如下:

更正后的代码

function addYears(dt, n) {
  var result = new Date(dt);
  result.setFullYear(result.getFullYear() + n);
  if (result.getMonth() !== dt.getMonth())
    result.setDate(result.getDate() - 1);
  return result;
}

// example usage
var result = addYears(dt, 1);

JavaScript有许多日期/时间库,例如Luxon, Date-Fns, Momentjs-Joda。所有这些库已经为它们的add-years函数使用了二月底的行为。除非您想要三月初的行为,否则不需要进行任何额外的调整。


0

我刚刚发现了一个有趣的问题。这是另一个支持尽可能使用UTC的理由。

// **** Appears to "Leap" forward **** //
moment('2020-02-29T00:00:00Z').toISOString();
// or
moment('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z

moment('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-03-01T00:00:00.000Z


// **** Falls back **** //
moment.utc('2020-02-29T00:00:00Z').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z

moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-02-28T00:00:00.000Z

鉴于C#的默认行为是回退...

DateTime dt = new DateTime(2020, 02, 29, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.AddYears(1);
// 2021-02-28T00:00:00.0000000Z

这可能是至关重要的,以确保前端和后端在回退或前进一天方面达成一致。


有趣,但我认为这只是证明了一年(或一个月或一天)添加的确切时间取决于时刻对象所处的模式。在第一个示例中,模式仍然是“本地”,即使您使用Z创建它。由于toISOString也输出UTC,因此被掩盖了。如果您省略了Z,或者使用format进行输出(或两者都使用),则行为将更清晰。(它实际上并没有向前跳跃。) - Matt Johnson-Pint
1
非常正确,正是底层的UTC标志让我在测试中使用了moment.utc()。因此,这可能更多地涉及到通过API进行反序列化/序列化的意识。也许我使用了错误的函数来说明上下文。对于那些没有使用moment.utc()从JSON初始化日期然后再次转换的人来说,这是一个容易犯的错误。 - rspring1975

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