蔡勒公式(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 | 见下一节的特殊规则 |
d | 日 | 1–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)/5 与 26(m+1)/10 数值相同)。英文版里 h 的映射约定是 0=星期六、1=星期日、…、6=星期五,与中文常用约定错位一天,换算时需留意。
关键规则:1 月、2 月归属前一年
这是初学者最容易踩坑的一步。蔡勒公式把 3 月视为一年的"第 1 个月",因此:
- 3 月–12 月:
m直接取 3–12,年份不变; - 1 月、2 月:
m分别取 13、14,并且年份减 1。
例如 2026 年 1 月 1 日,要按 2025 年的 13 月 1 日 代入计算,即 c = 20、y = 25、m = 13、d = 1。
这样设计是为了把闰日(2 月 29 日)放到"年尾"。2 月成为上一年的最后一个月后,闰年多出的那一天只会影响它之后(也就是下一年的 3 月起)的星期偏移,公式中只需用 ⌊y/4⌋ 一项统一处理闰年,而无需在计算 1 月、2 月时分情况讨论。
儒略历与 1582 年分界线
公元 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 71582 年 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),这一项把这个差异吸收掉。
示例演算:2026 年 7 月 21 日
代入公式:
- 年月日:
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⌋ = 20d − 1 = 20
- 求和:
26 + 6 + 5 − 40 + 20 + 20 = 37 - 取模:
37 mod 7 = 2,对应星期二。
(对照日历验证:2026-07-21 确实是星期二。)
代码实现(Python)
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 同余把多周期叠加、格里高利历与儒略历的世纪修正不同——就能既会心算也会编程实现,顺便也能看懂蔡志忠那类"快速报星期"的表演背后是什么。