给日期加月份比听起来更难

给某个日期加7天很简单:算出日期,再加7就行。加“一个月”则完全是另一个问题。

麻烦在于不同月份天数不同。1月有31天,2月有28天,有时是29天。如果你在1月31日再加一个月,就会落到2月31日——而这个日期并不存在。

几乎每个日历库、电子表格、数据库都会对“接下来该怎么办”有自己的处理方式。

大多数工具怎么做

最常见的行为是“贴齐”到目标月份的最后一天。1月31日 + 1个月 = 2月28日(闰年则是29日)。3月31日 + 1个月 = 4月30日。

这通常被称为 月末钳制(end-of-month clamping),也是 Excel、Google Sheets、Python 的 dateutil 以及多数日期库的默认做法。

它很合理,但会带来一个细微问题:这个操作不可逆。你把1月31日加一个月得到2月28日;再减一个月会回到1月28日——而不是1月31日。你“丢掉”了3天。

溢出(overflow)做法

有些系统允许日期“溢出”到下一个月,而不是钳制到月末。1月31日 + 1个月 = 3月3日(闰年则可能是3月2日,因为2月有29天)。

这样能保持总天数一致,但结果落在了你可能完全不想要的月份里。从用户角度看很反直觉,而且通常是错的。

某些数据库在使用 SQL 的 INTERVAL 语法时会出现这种行为(取决于配置)。如果你不知道自己处于哪种规则下,很容易踩坑。

加年份也有同样问题

2月29日只在闰年存在。把2024年2月29日加一年会得到2025年2月29日——同样不存在。钳制规则会给出2025年2月28日。

同样的行为,同样的取舍。

这会在什么时候真的引发 bug

订阅扣费是经典例子。用户在1月31日订阅,下次扣费日期是2月28日;然后是3月28日;再然后是4月28日。第一次之后的每个月,扣费都会比用户预期提前2–3天。

按月重复的日历事件也一样。“每月31号”在没有31号的月份里会悄悄变成“每月最后一天”。

贷款还款计划、发薪日,以及任何带有“每月”重复规则的东西,最终都会遇到这个边界情况。

加天数没有这个问题

如果你要表达的是“从现在起30天”,而不是“从现在起1个月”,那就直接加30天。结果明确且可逆。

这一区别很重要:30天周期的扣费和按月扣费不是一回事,时间一长差异会迅速累积。

在你的工具或库里需要确认什么

在任何系统里依赖日期加法之前,最好先弄清楚:

  • 在月末日期上,它是钳制还是溢出?
  • 多次相加时,它会保持“日”不变,还是每次都会重新钳制?
  • 遇到闰年边界(比如 2月29日 + 1年)会怎样?

日期计算器 可以显示任意日期与偏移量的精确结果——在你把计算写进代码之前,用它做一次 sanity check 很有帮助。