目录

蔡勒公式(Zeller's Congruence)

用一行同余式把任意日期换算成星期几

短视频里刷到漫画家蔡志忠说他能根据任意日期迅速说出是星期几,好奇心被勾了起来,顺手查了一下。这类"心算星期"背后的通用方法,就是本文要介绍的蔡勒公式(Zeller’s congruence)——给定一个日期,代入一行同余式即可算出它是星期几。

蔡勒公式由 19 世纪德国数学家克里斯提安·蔡勒(Christian Zeller)于 1882 年发表,其核心是把"日、月、年、世纪"对星期几的贡献都折算成模 7 的同余再相加。

格里高利历(Gregorian calendar,即通用的公历),最常见的写法是:

w = ( y + ⌊y/4⌋ + ⌊c/4⌋ − 2c + ⌊26(m+1)/10⌋ + d − 1 ) mod 7

其中各符号的含义如下:

符号含义备注
w星期结果0=星期日,1=星期一,…,6=星期六
c世纪数,即年份前两位如 2026 年 → c = 20
y年份后两位如 2026 年 → y = 26
m月份,取值 3–14见下一节的特殊规则
d1–31
⌊ ⌋高斯符号(向下取整 / floor)⌊26/10⌋ = 2
mod 7对 7 取余把结果归一到 0–6

英文文献中常把变量改记为 q (日)、K (年后两位)、J (世纪),并写作 h = ( q + ⌊13(m+1)/5⌋ + K + ⌊K/4⌋ + ⌊J/4⌋ − 2J ) mod 7,只是符号不同,公式完全等价(注意 13(m+1)/526(m+1)/10 数值相同)。英文版里 h 的映射约定是 0=星期六、1=星期日、…、6=星期五,与中文常用约定错位一天,换算时需留意。

这是初学者最容易踩坑的一步。蔡勒公式把 3 月视为一年的"第 1 个月",因此:

  • 3 月–12 月:m 直接取 3–12,年份不变;
  • 1 月、2 月:m 分别取 13、14,并且年份减 1

例如 2026 年 1 月 1 日,要按 2025 年的 13 月 1 日 代入计算,即 c = 20y = 25m = 13d = 1

这样设计是为了把闰日(2 月 29 日)放到"年尾"。2 月成为上一年的最后一个月后,闰年多出的那一天只会影响它之后(也就是下一年的 3 月起)的星期偏移,公式中只需用 ⌊y/4⌋ 一项统一处理闰年,而无需在计算 1 月、2 月时分情况讨论。

公元 1582 年,教宗额我略十三世(Pope Gregory XIII)颁布新历法,把 1582 年 10 月 4 日的次日直接定为 10 月 15 日,以修正儒略历(Julian calendar)长期累积的 10 天误差,并调整闰年规则(世纪年须被 400 整除方为闰年)。于是:

  • 1582 年 10 月 4 日及之前 用儒略历版公式:

    w = ( y + ⌊y/4⌋ − c + ⌊26(m+1)/10⌋ + d + 4 ) mod 7
  • 1582 年 10 月 15 日及之后 用上文的格里高利历版公式。

两者差别只在"世纪修正项":格里高利历版多了 ⌊c/4⌋ − 2c 一段来兑现"逢 400 年 97 闰"的规则;儒略历每 4 年一闰、没有世纪年例外,所以只保留 −c。历史上 1582 年 10 月 5 日至 14 日这 10 天从未存在过。

把公式拆开看,每一项都对应一个周期性的星期偏移:

  • d:每过 1 天,星期往后推 1。
  • ⌊26(m+1)/10⌋:月份修正。从 3 月起,各月天数模 7 大致呈 3, 2, 3, 2, 3 的交替模式,26/10 = 2.6 配合向下取整刚好刻画了"每 5 个月里有 2 个连着的 31 天月(7–8 月、12–1 月)“这一现象。
  • y:平年 365 天,365 mod 7 = 1,所以每过一个平年星期往后推 1。
  • ⌊y/4⌋:闰年多 1 天,每 4 年补 1。
  • ⌊c/4⌋ − 2c:世纪修正(格里高利历特有)。一个正常世纪是 36 524 天(mod 7 = 5),整除 400 的世纪是 36 525 天(mod 7 = 6),这一项把这个差异吸收掉。

代入公式:

  • 年月日:c = 20,y = 26,m = 7,d = 21(7 月无需调整)
  • 逐项计算:
    • y = 26
    • ⌊y/4⌋ = ⌊26/4⌋ = 6
    • ⌊c/4⌋ = ⌊20/4⌋ = 5
    • −2c = −40
    • ⌊26(m+1)/10⌋ = ⌊26 × 8 / 10⌋ = ⌊20.8⌋ = 20
    • d − 1 = 20
  • 求和:26 + 6 + 5 − 40 + 20 + 20 = 37
  • 取模:37 mod 7 = 2,对应星期二

(对照日历验证:2026-07-21 确实是星期二。)

def zeller(year: int, month: int, day: int) -> int:
    """返回 0=周日, 1=周一, ..., 6=周六。适用于格里高利历(1582-10-15 起)。"""
    if month < 3:
        month += 12
        year -= 1
    c, y = divmod(year, 100)
    w = (y + y // 4 + c // 4 - 2 * c + 26 * (month + 1) // 10 + day - 1) % 7
    return w

# 示例
print(zeller(2026, 7, 21))   # 输出 2 -> 星期二
print(zeller(2000, 1, 1))    # 输出 6 -> 星期六
  • 负数取模:−2c 一项可能让整个表达式变为负数。Python、Ruby 的 % 是数学同余(结果非负),直接用没问题;但 C/C++/Java/JavaScript 的 % 是"取余”,负数会返回负值,需写成 ((w % 7) + 7) % 7 做二次归一化,或者改用等价形式 + 5J 替换 −2J(5 ≡ −2 (mod 7))把整个表达式先变成非负。
  • 月份边界:别忘记把 1 月、2 月挪到上一年,否则结果会错一天。
  • 历法分界:若要处理 1582 年 10 月之前的日期(如历史事件),需切换到儒略历版公式;对公元 1 年之前的日期还要注意天文纪年法中的"公元 0 年"。对绝大多数日常场景,格里高利历版足够。

蔡勒公式本质是一个把"日、月、年、世纪"四类周期对星期的影响压进同一同余式的精巧组合。理解了三件事——1/2 月归属前一年mod 7 同余把多周期叠加格里高利历与儒略历的世纪修正不同——就能既会心算也会编程实现,顺便也能看懂蔡志忠那类"快速报星期"的表演背后是什么。