日期处理是软件开发中最最常见的一项需求,尤其是在需要计算不同日期之间天数差异的场景下。准确处理日期,主要是考虑了闰年、月份天数变化等因素之后。
我们首先定义了一个Date
结构,包含年、月、日三个成员变量。结构体简单明了,适用于存储日期信息:
cpp
struct Date { int y, m, d; };
闰年的判断
在日期计算中,闰年会影响2月的天数。根据现行的格里高利历法,闰年有以下规则:
- 年份能被400整除的是闰年。
- 年份能被4整除且不能被100整除的也是闰年。
- 其他年份为平年。
我们通过以下leap()
函数来判断给定的年份是否是闰年:
cpp
bool leap(int y) {
return y % 400 == 0 || (y % 4 == 0 && y % 100 != 0);
}
但是对于过往日期的处理,可能出现不同的规则。首先,格里高利历法自 1582 年才生效。在更早的儒略历中,每 4 年设为闰年,无论这个年份是否能被 100 整除,也不考虑是否能被 400 整除。所以 1500 年也是闰年。
另外,儒略历自公元前 45 年起开始使用,且不存在 0 年("公元前 1 年"的下一年是"公元 1 年")。那么公元前 1 年是平年还是闰年呢?按照 4 年为闰年的定律。公元前1年 、公元前5年 、公元前9年是闰年。
但因当时僧侣错误理解"隔三年设置一闰年",而是每三年设置了一个闰年。奥古斯都为了纠正了以上闰年过多的错误,故取消12年之间三次的闰年,拟补累积误差的天数。此后才按儒略历原来的设计,每四年有一次闰年。这就意味着公元前 45 年到公元后的有一段时间是三年一闰,然而,此间究竟何年是平年或者闰年已不可考。
简单起见,本代码假定从 1600 年 1 月 1 日开始。不考虑这些情况。
某个月份的天数
不同的月份有不同的天数:七月及以前,奇数月 31 天,偶数月 30 天;七月以后,奇数月 30 天,偶数月 31 天,2 月被单独处理,平年 28 天,闰年多一天为 29 天。
cpp
static const int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30};
int day(int y, int m) {
return days[m] + (m == 2 && leap(y)); // 闰年时,2月为29天
}
在公历改革之前,欧洲大多数地区使用的是儒略历,而这个日历系统存在一个问题:每年约有11分钟的偏差(儒略历年比实际太阳年长11分钟)。随着时间的推移,这个偏差累计起来,导致了季节和日期不再对齐。
为了修正这个问题,1582 年 10 月 15 日(格里高利历改革实施的日期)前的10天被"跳过",也就是从10月4日直接跳到10月15日,这样就恢复了季节的正确对齐。这被沿用至今,这导致 1582 年的 10 月只有 21 天。不过大多数日期库默认不考虑此情况。一些高级库可能存在此功能,本代码不考虑这个情况。
将日期转换为天数
为了方便计算日期之间的天数差,我们需要将日期转换为从 1600 年 1 月 1 日到指定日期的总天数。
cpp
static const int days_sum[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
int to_days(const Date &date) {
auto [y, m, d] = date;
return 365 * (y - 1600) + (y - 1600 + 3) / 4 - (y - 1600 + 99) / 100 + (y - 1600 + 399) / 400
+ days_sum[m - 1] + d - 1 + (m > 2 && leap(y));
}
让我们详细解析这个函数:
-
计算 \([1600, y)\) 每一年份的天数之和:
365 * (y - 1600)
计算从 1600 年到指定年份的所有年份的基础天数(每年365天)。(y - 1600 + 3) / 4
计算 \(4n\) 年份的数目,他们是闰年,每个闰年有1天额外天数。(y - 1600 + 99) / 100
计算 \(100n\) 年份的数目,这些年份是平他们也是 4 倍数,但不应该被加上 1 天。(y - 1600 + 399) / 400
计算 \(400n\) 年份的数目,这些年份是闰年,他们也是 100 倍数,但应该被加上 1 天。
-
计算 \(y-01-01\) 到 \(y-m-d\) 的天数:
days_sum[m - 1]
提供了 $ [1, m) $ 所有月份的天数和。days_sum
是一个数组,存储每个月前的累计天数。d - 1
即本月的天数,是当前月份的日期减去1,因为我们以 \(1600-01-01\) 为第 0 天。
-
处理2月的额外天数:
(m > 2 && leap(y))
如果月份大于2,并且当前年份是闰年,则在2月后增加1天(2月29日)。