日期处理问题

日期处理是软件开发中最最常见的一项需求,尤其是在需要计算不同日期之间天数差异的场景下。准确处理日期,主要是考虑了闰年、月份天数变化等因素之后。

我们首先定义了一个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));
}

让我们详细解析这个函数:

  1. 计算 \([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 天。
  2. 计算 \(y-01-01\) 到 \(y-m-d\) 的天数

    • days_sum[m - 1] 提供了 $ [1, m) $ 所有月份的天数和。days_sum是一个数组,存储每个月前的累计天数。
    • d - 1 即本月的天数,是当前月份的日期减去1,因为我们以 \(1600-01-01\) 为第 0 天。
  3. 处理2月的额外天数

    • (m > 2 && leap(y)) 如果月份大于2,并且当前年份是闰年,则在2月后增加1天(2月29日)。