蔡勒(Zeller)公式及其推导:快速将任意⽇期转换为星期数
0. 本⽂的初衷及蔡勒公式的⽤处
前⼀段时间,我在准备北邮计算机考研复试的时候,做了⼏道与⽇期计算相关的题⽬,在这个过程中我接触到了蔡勒公式。先简单的介绍⼀下蔡勒公式是⼲什么⽤的。
我们有时候会遇到这样的问题:看到⼀个⽇期想知道这⼀天是星期⼏,甚⾄看到⼀个历史⽇期或纪念⽇,我们想快速的知道这⼀天是星期⼏。对于这个问题,如果⽤编程的⽅式,应该怎么实现呢?你可能已经有思路了,⽐如你知道某个⽇期是星期⼏,把这个⽇期作为原点,然后计算⽬标⽇期和这个原点之间相差多少天,再除以7 求余数,最后通过余数判断⽬标⽇期的星期数。通过这样的过程,你确实可以得到正确的结果,但这不够快速也不够优雅。对于这个问题,如果你懂得蔡勒公式,那就变得异常简单了,你只需要将年⽉⽇等数据代⼊公式,然后计算出结果,这⼀结果就是⽬标⽇期对应的星期数。
当我知道蔡勒公式之后,我觉得它⾮常有趣也很酷,所以我不仅希望掌握公式的使⽤,也希望可以掌握公式背后的推导过程。然⽽,当我在⽹上搜索相关的⽂章时,我发现⼏乎所有向我展⽰的博客(从零⼏年到最近的 19 年)⼤多是转载、复制于这篇⽂章():
星期制度是⼀种有古⽼传统的制度。据说因为《圣经·创世纪》中规定上帝⽤了六天时间创世纪,第七天休息,所以⼈们也就以七天为⼀个周期来安排⾃⼰的⼯作和⽣活,⽽星期⽇是休息⽇……
这篇⽂章质量很不错,讲解过程⾃然流畅,但是在⼀些细节上存在错误,有些推导步骤让⼈感到困惑。因此,当我掌握蔡勒公式后,很希望可以将我的理解输出出来,让想要学习蔡勒公式推导过程的⼈看到⼀些新的材料。好了,废话少说,我们开始吧。
1. 蔡勒公式的形式
如果你对公式的推导过程不感兴趣,只是希望使⽤蔡勒公式,那么只看此⼩节即可。蔡勒公式的形式如下:
\[\begin{aligned} D &= \left[ \frac{c}{4} \right] - 2c + y + \left[ \frac{y}{4} \right] + \left[ \frac{13(m+1)}{5} \right] + d - 1 \\[2ex] W &= D \bmod 7 \end{aligned} \]
其中:
W 是星期数。
c 是世纪数减⼀,也就是年份的前两位。
y 是年份的后两位。
m 是⽉份。m 的取值范围是 3 ⾄ 14,因为某年的 1、2 ⽉要看作上⼀年的 13、14⽉,⽐如 2019 年的 1 ⽉ 1 ⽇要看作 2018 年的 13 ⽉ 1 ⽇来计算。
d 是⽇数。
[] 是取整运算。
mod 是求余运算。
注意:这些符号在后⾯的推导中还会使⽤。举⼀个实际的计算例⼦:计算 1994 年 12 ⽉ 13 ⽇是星期⼏。显然 c = 19,y = 94,m = 12,d = 13,带⼊公式:
\[\begin{aligned} D &= \left[ \frac{19}{4} \right] - 2 \times 19 + 94 + \left[ \frac{94}{4} \right] + \left[ \frac{13 \times (12 + 1)}{5} \right] + 13 - 1 \\[2ex] &= 4 - 38 + 94 + 23 + 33 + 13 - 1 \\[2ex] &= 128 \\[2ex] W &= 128 \bmod 7 = 2 \end{aligned} \]
由此可得 1994 年 12 ⽉ 13 ⽇是星期⼆,通过查询⽇历可以验证正确性。
最后关于蔡勒公式,还需要做两点补充说明:
1. 在计算机编程中,W 的计算结果有可能是负数。我们需要注意,数学中的求余运算和编程中的求余运算不是完全相同的,数学上余数不能是负数,⽽编程中余
数可以是负数。因此,在计算机中 W 是负数时,我们需要进⾏修正。修正⽅法⼗分简单:让 W 加上⼀个⾜够⼤的 7 的整数倍,使其成为正数,得到的结果再对 7 取余即可。⽐如 -15,我可以让其加上 70,得到 55,再除以 7 余 6,通过余数可知这⼀天是星期六。
2. 此公式只适⽤于格⾥⾼利历(也就是现在的公历)。关于历法的问题,⼤家有兴趣可以⾃⾏查阅。
下⾯是公式的推导。
2. 推导过程
推导蔡勒公式之前,我们先思考⼀下,如果我们不知道这⼀公式,我们如何将⼀个⽇期转化为星期数呢?
我们可能会很⾃然地想到:先到⼀个知道是星期⼏的⽇⼦,把这个⽇期作为“原点”,然后计算⽬标⽇期和这个原点相差⼏天,将相差的天数对 7 取余,再根据余数判断星期数。举⼀个实际例⼦,⽐如我们知道 2019 年 5 ⽉ 1 ⽇是星期三,把这⼀天当作原点,现在我们想知道 2019 年 5 ⽉ 17 ⽇是星期⼏,显然这两个⽇期之间相差 16 天,⽤ 16 除 7 余 2,因为原点是星期三,加上作为偏移量的余数 2,可知
2019 年 5 ⽉ 17 ⽇是星期五。
从这个思路出发,经过优化扩展,我们就可以得到神奇的蔡勒公式了。⾸先,如果我们仔细观察⼀下可以发现,这个思路中⽐较⿇烦的是计算相差天数(设为 \
(D\)),我们可以把 \(D\) 的计算过程分解成三部分:
1. \(D_1\):从原点到原点所在年份末尾的天数。
2. \(D_2\):原点所在年份和⽬标⽇期所在年份之间所有年份的天数和。
3. \(D_3\):⽬标⽇期所在年份的第⼀天到⽬标⽇期的天数。
显然,\(D = D_1 + D_2 + D_3\)。如果我们把原点选择在某⼀年的 12 ⽉ 31 ⽇,那么就可以省去 \(D_1\) 的计算了,因为原点恰好就是所在年份的最后⼀天。现在,\ (D = D_2 + D_3\)。
我们再去观察上述思路中的实际例⼦,可以发现,因为原点是星期三,所以得到余数后,我们需要加上 3 才是正确的星期数。这启⽰我们可以把原点选定在星期⽇,这样算出来的余数是⼏就是星期⼏(余数 0 代表星期⽇)。
经过这样的分析。我们希望可以优化原点的⽇期,使其满⾜下⾯两个条件:
1. 是某⼀年的 12 ⽉ 31 ⽇。
2. 是星期⽇。
我们按照现在使⽤的公历的规则逆推,可以发现公元元年的前⼀年的 12 ⽉ 31 ⽇恰好是星期⽇,满⾜我们想要的两个条件,是⼀个完美的原点。
现在假设⽬标⽇期是 y 年 m ⽉ d ⽇,我们已经可以很容易的计算 \(D_2\) 了:
\[D_2 = (y-1) \times 365 + \left[ \frac{y-1}{4} \right] - \left[ \frac{y-1}{100} \right] + \left[ \frac{y-1}{400} \right] \]
简单的解释⼀下。因为⼀年最少有 365 天,所以 \(D_2\) ⾄少是 \((y-1) \times 365\)。此外,因为闰年⽐平年多⼀天,我们还需要加上这些年份中闰年的数量。按照闰年的规则:每 4 年⼀闰,但每 100 年不闰,
每 400 年⼜闰。可知闰年的数量为 \(\left[ \frac{y-1}{4} \right] - \left[ \frac{y-1}{100} \right] + \left[ \frac{y-1}{400} \right]\)。
现在,我们需要得到 \(D_3\) 的计算公式,这块要复杂⼀些。⾸先,不考虑闰年的话,每年中 2 ⽉份天数最少,为 28 天。因此,我们不妨把每个⽉的天数看作 “28 + Excess”的模式,m ⽉之前所有⽉份的 Excess 之和为 Accum(m),则 \(D_3 = 28 \times (m-1) + Accum(m) + d\),并且我们可以得到这样⼀张表格:
⽉份123456789101112
天数312831303130313130313031
Excess303232332323
Accum0336811131619212426
仔细观察,可以发现 Excess 从 3 ⽉份开始是 3、2、3、2、3 的循环,因此,当 \(m\geq 3\) 时,\(Accum(m)\) 的值的增幅也是 3、2、3、2、3 的循环。因为每 5 个⽉增加 13,所以把 \(\frac{13}{5}\) 作为系数;因为 \(Accum(m)\) 的值是离散的(都是整数),所以我们⽤取整运算符,得到:
\[f(x) = \left[ \frac{13}{5}x \right] \]
我们将 \(x\) 的值取 1,2,3……,然后观察 \(f(x)\) 的值,可得下⾯这张表格:
x4567891011121314
f(x)1013151820232628313336
我们可以发现,当 \(x \geq 4\) 时,\(f(x)\) 的值的增幅也是 3,2,3,2,3 的循环。也就是说 \(f(x)\) 的值的增幅(\(x \geq 4\))与 \(Accum(m)\) 的值的增幅(\(m \geq 3\))相同,这意味着 \(f(x)\) 与 \(Accum(m)\) 之间相差⼀个常数,我们随便带⼊⼀个具体的值计算:
\[f(4) - Accum(3) = 10 - 3 = 7 \]
可知相差的常数为 7。由此可得,当 \(m \geq 3\) 时,\(Accum(m)\) 的值的序列,等于当 \(x \geq 4\) 时,\(f(x) - 7\) 的值的序列。这样我们就得到了 \(Accum(m), m
\geq 3\) 的函数形式:
\[\begin{align*} Accum(m) &= f(m+1) - 7 \\[2ex] &= \left [ \frac{13(m+1)}{5} \right ] - 7 \end{align*} \]
这⾥多说两句,实际上,\(Accum(m)\) 的函数形式是不唯⼀的,使⽤其他的构造⽅法,可以得到形式不同的 \(Auccm(m)\),只要符合要求即可。
进⼀步,我们可以得到 \(D_3\) 的函数形式:
\[D_3 = \begin{cases} d, &m = 1 \\[2ex] 31 + d, &m = 2 \\[2ex] (m-1) \times 28 + \left[ \frac{13(m+1)}{5} \right] - 7 + d + i, &m \geq 3 \end{cases} \]
其中,平年时,\(i = 0\);闰年时,\(i = 1\)。这还不是 \(D_3\) 最完美的形式。我们继续分析,从 3 ⽉份到 12 ⽉份的 Excess 正好是两个 3、2、3、2、3 的循环,那么假如有第 13 ⽉,想要继续保持这种循环规律,13 ⽉的 Excess 值应该是 3。⽽ 1 ⽉份的 Excess 的值恰好是 3,所以我们不妨变通⼀下,把每年的 1 ⽉、2⽉当作上⼀年的 13⽉、14 ⽉。这样不仅仍然符合公式,⽽且 2 ⽉份变成了上⼀年的最后⼀个⽉,也就是公式中 \(d\) 的部分,于是平闰年的影响也去掉了,\(D_3\) 的公式简化成了:
\[D_3 = (m-1) \times 28 + \left[ \frac{13(m+1)}{5} \right] - 7 + d, \quad 3 \leq m \leq 14 \]
现在,我们已经得到了 \(D_2\) 和 \(D_3\) 的计算函数,由 \(D = D_2 + D_3\),可知:
\[D = (y-1) \times 365 + \left[ \frac{y-1}{4} \right] - \left[ \frac{y-1}{100} \right] + \left[ \frac{y-1}{400} \right] + (m-1) \times 28 + \left[ \frac{13(m+1)}{5} \right] - 7 + d, \quad 3 \leq m \leq 14 \]
注意!这个公式离正确形式还差⼀⼩步。因为在当前的公式中,每年的 1 ⽉和 2 ⽉被当作上⼀年的 13 ⽉和 14 ⽉计算,因此当前公式中计算闰⽇的部分(\(\left[
\frac{y-1}{4} \right] - \left[ \frac{y-1}{100} \right] + \left[ \frac{y-1}{400} \right]\))存在错误。举⼀个具体的例⼦,⽐如计算公元 4 年(闰年)3 ⽉ 1 ⽇的星期数。在当前公式中,公元 4 年的 2 ⽉被算作了公元 3 年的 14 ⽉(换句话说公元 3 年变成了闰年),⽽公式中计算闰⽇的部分没有考虑这点,依然将公元 3 年当成平年计算,因此少算了⼀天。因此,计算闰⽇的部分应当改进,公式如下:
\[D = (y-1) \times 365 + \left[ \frac{y}{4} \right] - \left[ \frac{y}{100} \right] + \left[ \frac{y}{400} \right] + (m-1) \times 28 + \left[ \frac{13(m+1)}{5} \right] - 7 + d, \quad 3 \leq m \leq 14 \tag{1} \]
计算出 D 的值后,对 7 取模即可得到星期数。
根据同余定理,D 除以 7 所得的余数等于 D 式的各项分别除以 7 所得余数之和(余数之和⼤于等于 7 时,再对 7 取余),因此可以消去 D 式中能被 7 整除的项,进⾏化简:
\[\begin{align*} D &= (y-1) \times 365 + \left[ \frac{y}{4} \right] - \left[ \frac{y}{100} \right] + \left[ \frac{y}{400} \right] + (m-1) \times 28 + \left[ \frac{13(m+1)}{5} \right] - 7 + d \\[2ex] &= (y-1) \times (364+1) + \left[ \frac{y}{4} \right] - \left[ \frac{y}{100} \right] + \left[ \frac{y}{400} \right] + \left[ \frac{13(m+1)}{5} \right] + d \\[2ex] &= (y-1) + \left[
\frac{y}{4} \right] - \left[ \frac{y}{100} \right] + \left[ \frac{y}{400} \right] + \left[ \frac{13(m+1)}{5} \right] + d \tag{2} \end{align*} \]
简单说明⼀下:
\[\begin{align*} (y-1) \times 365 &= (y-1) \times (364+1) \\[2ex] &= (y-1) \times 364 + (y-1) \\[2ex] &= (y-1) \times 52 \times 7 + (y-1) \end{align*} \]
显然,结果中的第⼀项是 7 的倍数,除以 7 余数为 0,因此 \((y-1) \times 365\) 除以 7 的余数其实就等于 \((y-1)\) 除以 7 的余数,我们只保留 \((y-1)\)就够了。化简过
程中,其他被销去的项同理。
公式(2)还不是最简练的形式,我们还可以对年份进⾏处理。我们现在⽤公式(2)计算出每个世纪第⼀年 3 ⽉ 1 ⽇的星期数,得到如下结果:
年份:1, 401, 801, … , 2001101, 501, 901, … , 2101201, 601, 1001, … , 2201301, 701, 1101, … ,2301
星期:4205
可以发现,每隔 4 个世纪,星期数就会重复⼀次。因为在数学上,-2 和 5 除以 7 的余数相同,所以我们不妨把这个重复序列中的 5 改为 -2。这样,4、2、0、-2 恰好构成了⼀个等差数列。利⽤等差公式,我们可以得到计算每个世纪第⼀年的 3 ⽉ 1 ⽇星期数的公式:
\[W = 4 - 2(c \bmod 4) \tag{3} \]
其中,\(c\) 是世纪数减⼀。我们把公式(2)和公式(3)联⽴,代⼊特定的⽇期——3 ⽉ 1 ⽇,可以得到:
\[((y-1) + \left[ \frac{y-1}{4} \right] - \left[ \frac{y-1}{100} \right] + \left[ \frac{y-1}{400} \right] + 11) \bmod 7 = 4 - 2(c \bmod 4) \]
利⽤同余定理,经过变换得到:
\[(y-1) + \left[ \frac{y-1}{4} \right] - \left[ \frac{y-1}{100} \right] + \left[ \frac{y-1}{400} \right] \equiv -2(c \bmod 4) \quad (\bmod 7) \tag{4} \]
其中,\(\equiv\) 是表⽰同余的符号,括号中 \(\bmod 7\) 的意思是指 \(\equiv\) 两边的数除以 7 得到的余数相同。根据公式(4),我们可以知道在每个世纪的第⼀年,\((y-1) + \left[ \frac{y-1}{4} \right] - \left[ \frac{y-1}{100} \right] + \left[ \frac{y-1}{400} \right]\) 可以被 \(-2(c \bmod 4)\) 同余替换。进⽽计算 \(D\) 的公式得到如下形式:
\[D = -2(c \bmod 4) + \left[ \frac{13(m+1)}{5} \right] + d \tag{5} \]
什么是编程举个例子
注意!现在的计算公式只能适⽤于每个世纪的第⼀年。但是,有个这个公式,再加上计算⼀个世纪中闰⽇的部分,我们就可以很容易地得到计算这个世纪其他年份的⽇期的星期数的公式了。令 c 等于世纪数减⼀,y 等于世纪中的年份数(如 1994 年,则 c = 19,y = 94)。因为⼀个世纪中只有⼀百年,所以不⽤考虑“四百年⼜闰”的情况;因为每百年,即每个世纪最后⼀年的 y = 00,⽽ \(\left[ \frac{y}{4} \right]_{y=0} = 0\),所以 \(\left[ \frac{y}{4} \right]\) 既可以计算四年⼀闰的情况,⼜满⾜百年不闰的要求。综合这些情况,与得到公式(2)的过程类似,我们可以得到:
\[D = -2(c \bmod 4) + (y-1) + \left[ \frac{y}{4} \right] + \left[ \frac{13(m+1)}{5} \right] + d \tag{6} \]
在公式(6)中,\(y\) 是年份的后两位。
最后,我们来把公式中的取模运算改成四则运算。设商为 \(q\),余数为 \(r\),则:
\[4q + r = c \]
⼜因为,
\[\begin{align*} q &= \left[ \frac{c}{4} \right] \\[2ex] r &= c \bmod 4 \end{align*} \]
可得:
\[c \bmod 4 = c - 4 \times \left[ \frac{c}{4} \right] \]
代⼊公式(6)可得:
\[D = \left[ \frac{c}{4} \right] - 2c + y - 1 + \left[ \frac{y-1}{4} \right] + \left[ \frac{13(m+1)}{5} \right] + d \tag{7} \]
⾄此,我们就得到了蔡勒公式的最终形式。