PAYATHON 2026

Python relativedelta 为什么会把日期区间算成 1 个月?

数据小许

结论

relativedelta 没有算错。它会把两个日期的差分解成年、月、日,而不是换算成总天数。你的代码在分解前执行了 d2 += timedelta(days=1),把 2025-12-22 调整为 2025-12-23。从 2025-11-23 到 2025-12-23 正好是一个日历月,所以结果是 1 month。

你列出的预期用了两种规则:2025-11-23 至 2025-12-22 按首尾都包含计算,结果是 30 天;而 2025-11-23 至 2025-12-31 预期为 1 month and 8 days,这是不把结束日期加一天后进行日历分解的结果。需要先确定“月和天”采用哪种表示约定,再统一处理边界。

为什么会出现这些结果

原代码的关键逻辑是:

if include_end:
    d2 = d2 + timedelta(days=1)

r = relativedelta(d2, d1)

这几个日期分别会得到以下结果:

  • 2025-11-23 到 2025-12-21 相差 28 天,首尾都计入时为 29 天。调整后的结束日期是 2025-12-22,还没到一个月的日历周年,因此结果是 29 days。
  • 2025-11-23 到 2025-12-22 相差 29 天,首尾都计入时为 30 天。但代码把结束日期加一天,改为 2025-12-23。因此 relativedelta(2025-12-23, 2025-11-23) 正好是 1 month。
  • 2025-11-23 到 2025-12-31 不调整时是 1 month and 8 days。加一天后,结束日期变为 2026-01-01,与 2025-11-23 相差 1 month and 9 days。

2025-12-22 的结果变化,是因为结束日期加一天后刚好跨过了月周年。2025-12-31 多出的那一天也来自同一处调整。

按需要选择计算方式

只需要总天数,并且首尾日期都计入

要计算首尾都包含的总天数,直接用 timedelta,不要用 relativedelta 表示结果:

from datetime import date, timedelta

def inclusive_days(d1: date, d2: date) -> int:
    if d2 < d1:
        d1, d2 = d2, d1
    return (d2 - d1).days + 1

print(inclusive_days(date(2025, 11, 23), date(2025, 12, 22)))
# 30

需要按日历单位显示年、月、日

如果用 relativedelta 按日历单位分解,就不要为了“包含结束日”而先把 d2 加一天:

from dateutil.relativedelta import relativedelta

r = relativedelta(date(2025, 12, 31), date(2025, 11, 23))
print(r.months, r.days)
# 1 8

结果是提问中期望的 1 month and 8 days。但按这条规则,2025-11-23 至 2025-12-22 会得到 29 days,而不是首尾都计入的 30 days。

如果产品需要“未满一个完整日历月时,按首尾包含的总天数显示;达到完整日历月后,再显示日历月和剩余天数”,可以先判断是否已经满一个日历月,再分别计算:

from datetime import date
from dateutil.relativedelta import relativedelta

def inclusive_duration(d1: date, d2: date) -> tuple[int, int, int]:
    if d2 < d1:
        d1, d2 = d2, d1

    r = relativedelta(d2, d1)
    if r.years == 0 and r.months == 0:
        return 0, 0, (d2 - d1).days + 1

    return r.years, r.months, r.days

按这段代码,2025-11-23 至 2025-12-22 会显示为 30 天,2025-11-23 至 2025-12-31 会显示为 1 个月零 8 天。这种展示约定需要明确:出现完整日历月后,剩余天数按未经调整的结束日期计算,不再额外加一天。

还需检查的代码细节

get_my_tuple 的同月分支直接计算 abs(d2.day - d1.day),但此时日期可能已经按 include_start、include_end 调整过。结果是否符合预期,取决于这两个开关是否表示“把区间端点计入”,以及同月日期的处理方式。建议先统一端点规则,再让同月和跨月路径遵循同一口径,避免结果不一致。

另外,relativedelta 的月分量表示日历月,不是固定天数。每个月的天数不同,因此不能把“1 month”当作固定天数,也无法将它唯一换算成总天数。

备注:内容仅供参考。