给日期加月份比听起来更难
给某个日期加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 很有帮助。