SQLite 加一个月:一月三十一日,为什么会算到三月二日

前天 3阅读

给一月三十一日加一个月,原本期待二月底,SQLite 却返回三月二日。这里不能把一个月当成固定天数,也不能假定所有日期库都会自动夹到目标月份末日。月份长度不同会产生无对应日期的情况,需要先约定遇到月底时采用哪条规则。

本文选用闰年的二零二四年,让二月二十九日参与边界测试。使用 date 的 floor 修饰符需要 SQLite 3.46.0 或更新版本;完整 Python 示例先检查实际连接库版本。保存为 demo.py 后运行,只执行内存查询,不创建或修改业务表。

默认溢出与夹到月底,是两种合法约定

SQLite 加一个月:一月三十一日,为什么会算到三月二日

AI概念示意图:月底标记在不同长度的月份之间移动,分支表示两种处理歧义的选择。图片不是运行截图。

import sqlite3

assert sqlite3.sqlite_version_info >= (3, 46, 0)
con = sqlite3.connect(":memory:")
try:
    def date(value, *modifiers):
        params = (value,) + modifiers
        placeholders = ",".join("?" for _ in params)
        return con.execute("SELECT date(" + placeholders + ")", params).fetchone()[0]

    overflow = date("2024-01-31", "+1 month")
    clamped = date("2024-01-31", "+1 month", "floor")
    assert overflow == "2024-03-02"
    assert clamped == "2024-02-29"
    assert date("2025-01-31", "+1 month", "floor") == "2025-02-28"
    print("default:", overflow)
    print("floor:", clamped)

    after_shift = date("2024-01-31", "+1 month", "start of month")
    before_shift = date("2024-01-31", "start of month", "+1 month")
    assert after_shift == "2024-03-01"
    assert before_shift == "2024-02-01"
    assert date("2024-02-15", "start of month", "+1 month", "-1 day") == "2024-02-29"
    assert date("2024-03-31", "start of month", "-1 day") == "2024-02-29"
    print("month-start after shift:", after_shift)
    print("month-start before shift:", before_shift)

    chained = date(clamped, "+1 month", "floor")
    anchored = date("2024-01-31", "+2 months", "floor")
    assert chained == "2024-03-29"
    assert anchored == "2024-03-31"
    print("chained:", chained)
    print("original anchor:", anchored)
finally:
    con.close()

一月三十一日加一个月,默认结果为三月二日;紧接着加 floor 则得到二月二十九日。默认采用 ceiling 方式处理歧义,floor 选择较早的月底结果。代码同时测试非闰年的二零二五年,得到二月二十八日,避免只靠一个闰年样本推断所有年份。

floor 不是对所有日期都“取月底”,它用于解决紧邻的月份或年份移动所产生的歧义。若原本有合法对应日,仍应按相应日期理解。业务若需要每个月最后一天,应直接表达月末规则,而不是把某次成功得到月底的结果当成通用公式。

先后顺序会改变下一步处理的月份

修饰符从左向右应用。一月三十一日先加一个月,已经变成三月二日,再取 start of month 得到三月一日。反过来先取一月一日,再加一个月,则得到二月一日。两个表达式包含相同文字,顺序不同却是在不同中间日期上继续计算。

若需求是“当前月份的最后一天”,可以先取月初,加一个月到下月月初,再减一天。若要“三月这一天所在月份的上个月末”,先取三月月初再减一天即可。示例分别断言二月月末和三月之前的月末,直接对照业务目标,减少含糊的“往前一个月”说法。

连续夹到月底,可能丢失原始日期锚点

从一月三十一日加一个月并 floor,得到二月二十九日;再从这个结果加一个月并 floor,得到三月二十九日。第二次计算已经不知道原始计划是三十一号。若从原始一月三十一日直接加两个月,结果则是三月三十一日。

因此,为固定月日的重复计划生成日期时,应保存原始锚点和期数,按每一期从锚点计算;若规则是“上次实际发生日再往后一个月”,才使用链式结果。两种排期都有实际用途,程序不能靠默认函数行为替业务决定。遇到短月后是否恢复原来的日号,尤其需要明确。

日期、时间点和时长分别测试

本例输入都是纯日历日期,没有本地时间转换和夏令时问题,也不把月份运算用于计算经过了多少秒。需要精确经过天数或秒数时,应选择对应的差值方法;需要某地预约时刻时,还需另行定义时区与当地不存在或重复的时间如何处理。

回归样本至少包含月初、三十日、三十一日、闰年二月和非闰年二月,并同时验证单步、两步链式与按原锚点计算。看到返回了格式正确的日期还不够,必须核对它落在哪个月、是否符合约定的日号以及下一期能否继续保持同一规则。

资料核对日期:2026年10月2日(北京时间)。示例在 CPython 3.12.14、SQLite 3.53.1 中独立运行。

参考资料

SQLite 官方文档:日期修饰符与 floor、ceiling

SQLite 3.46.0 发布说明:新增月份歧义控制

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。