計算給定日期是星期幾,好象是程式設計都會遇到的問題,最近論壇裡也有人提到這個問題,并給出了一個公式:
W= (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400) mod 7
(要求将1、2月當作上一年的13、14月來計算)
去看了看這個公式的原帖
其講述的過程并不清楚,便想怎樣自己推導出一個公式來,花了幾個小時,總算是弄出來了,結果跟上面的公式一樣:)
==============================================================================================
0. 本文的初衷及蔡勒公式的用處
前一段時間,我在準備北郵計算機考研複試的時候,做了幾道與日期計算相關的題目,在這個過程中我接觸到了蔡勒公式。先簡單的介紹一下蔡勒公式是幹什麼用的。
我們有時候會遇到這樣的問題:看到一個日期想知道這一天是星期幾,甚至看到一個曆史日期或紀念日,我們想快速的知道這一天是星期幾。對于這個問題,如果用程式設計的方式,應該怎麼實作呢?你可能已經有思路了,比如你知道某個日期是星期幾,把這個日期作為原點,然後計算目标日期和這個原點之間相差多少天,再除以 7 求餘數,最後通過餘數判斷目标日期的星期數。通過這樣的過程,你确實可以得到正确的結果,但這不夠快速也不夠優雅。對于這個問題,如果你懂得蔡勒公式,那就變得異常簡單了,你隻需要将年月日等資料代入公式,然後計算出結果,這一結果就是目标日期對應的星期數。
當我知道蔡勒公式之後,我覺得它非常有趣也很酷,是以我不僅希望掌握公式的使用,也希望可以掌握公式背後的推導過程。然而,當我在網上搜尋相關的文章時,我發現幾乎所有向我展示的部落格(從零幾年到最近的 19 年)大多是轉載、複制于這篇文章(連結):
星期制度是一種有古老傳統的制度。據說因為《聖經·創世紀》中規定上帝用了六天時間創世紀,第七天休息,是以人們也就以七天為一個周期來安排自己的工作和生活,而星期日是休息日……
這篇文章品質很不錯,講解過程自然流暢,但是在一些細節上存在錯誤,有些推導步驟讓人感到困惑。是以,當我掌握蔡勒公式後,很希望可以将我的了解輸出出來,讓想要學習蔡勒公式推導過程的人看到一些新的材料。好了,廢話少說,我們開始吧。
1. 蔡勒公式的形式
如果你對公式的推導過程不感興趣,隻是希望使用蔡勒公式,那麼隻看此小節即可。蔡勒公式的形式如下:
DW=[c4]−2c+y+[y4]+[13(m+1)5]+d−1=Dmod7D=[c4]−2c+y+[y4]+[13(m+1)5]+d−1W=Dmod7
其中:
- 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,帶入公式:
DW=[194]−2×19+94+[944]+[13×(12+1)5]+13−1=4−38+94+23+33+13−1=128=128mod7=2D=[194]−2×19+94+[944]+[13×(12+1)5]+13−1=4−38+94+23+33+13−1=128W=128mod7=2
由此可得 1994 年 12 月 13 日是星期二,通過查詢月曆可以驗證正确性。
最後關于蔡勒公式,還需要做兩點補充說明:
- 在計算機程式設計中,W 的計算結果有可能是負數。我們需要注意,數學中的求餘運算和程式設計中的求餘運算不是完全相同的,數學上餘數不能是負數,而程式設計中餘數可以是負數。是以,在計算機中 W 是負數時,我們需要進行修正。修正方法十分簡單:讓 W 加上一個足夠大的 7 的整數倍,使其成為正數,得到的結果再對 7 取餘即可。比如 -15,我可以讓其加上 70,得到 55,再除以 7 餘 6,通過餘數可知這一天是星期六。
- 此公式隻适用于格裡高利曆(也就是現在的公曆)。關于曆法的問題,大家有興趣可以自行查閱。
下面是公式的推導。
2. 推導過程
推導蔡勒公式之前,我們先思考一下,如果我們不知道這一公式,我們如何将一個日期轉化為星期數呢?
我們可能會很自然地想到:先找到一個知道是星期幾的日子,把這個日期作為“原點”,然後計算目标日期和這個原點相差幾天,将相差的天數對 7 取餘,再根據餘數判斷星期數。舉一個實際例子,比如我們知道 2019 年 5 月 1 日是星期三,把這一天當作原點,現在我們想知道 2019 年 5 月 17 日是星期幾,顯然這兩個日期之間相差 16 天,用 16 除 7 餘 2,因為原點是星期三,加上作為偏移量的餘數 2,可知 2019 年 5 月 17 日是星期五。
從這個思路出發,經過優化擴充,我們就可以得到神奇的蔡勒公式了。首先,如果我們仔細觀察一下可以發現,這個思路中比較麻煩的是計算相差天數(設為 DD ),我們可以把 DD 的計算過程分解成三部分:
- D1D1 :從原點到原點所在年份末尾的天數。
- D2D2 :原點所在年份和目标日期所在年份之間所有年份的天數和。
- D3D3 :目标日期所在年份的第一天到目标日期的天數。

顯然,D=D1+D2+D3D=D1+D2+D3 。如果我們把原點選擇在某一年的 12 月 31 日,那麼就可以省去 D1D1 的計算了,因為原點恰好就是所在年份的最後一天。現在,D=D2+D3D=D2+D3 。
我們再去觀察上述思路中的實際例子,可以發現,因為原點是星期三,是以得到餘數後,我們需要加上 3 才是正确的星期數。這啟示我們可以把原點標明在星期日,這樣算出來的餘數是幾就是星期幾(餘數 0 代表星期日)。
經過這樣的分析。我們希望可以優化原點的日期,使其滿足下面兩個條件:
- 是某一年的 12 月 31 日。
- 是星期日。
我們按照現在使用的公曆的規則逆推,可以發現公元元年的前一年的 12 月 31 日恰好是星期日,滿足我們想要的兩個條件,是一個完美的原點。
現在假設目标日期是 y 年 m 月 d 日,我們已經可以很容易的計算 D2D2 了:
D2=(y−1)×365+[y−14]−[y−1100]+[y−1400]D2=(y−1)×365+[y−14]−[y−1100]+[y−1400]
簡單的解釋一下。因為一年最少有 365 天,是以 D2D2 至少是 (y−1)×365(y−1)×365 。此外,因為閏年比平年多一天,我們還需要加上這些年份中閏年的數量。按照閏年的規則:每 4 年一閏,但每 100 年不閏,每 400 年又閏。可知閏年的數量為 [y−14]−[y−1100]+[y−1400][y−14]−[y−1100]+[y−1400] 。
現在,我們需要得到 D3D3 的計算公式,這塊要複雜一些。首先,不考慮閏年的話,每年中 2 月份天數最少,為 28 天。是以,我們不妨把每個月的天數看作 “28 + Excess”的模式,m 月之前所有月份的 Excess 之和為 Accum(m),則 D3=28×(m−1)+Accum(m)+dD3=28×(m−1)+Accum(m)+d ,并且我們可以得到這樣一張表格:
月份 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
天數 | 31 | 28 | 30 | |||||||||
Excess | ||||||||||||
Accum | 13 | 16 | 19 | 21 | 24 | 26 |
仔細觀察,可以發現 Excess 從 3 月份開始是 3、2、3、2、3 的循環,是以,當 m≥3m≥3 時,Accum(m)Accum(m) 的值的增幅也是 3、2、3、2、3 的循環。因為每 5 個月增加 13,是以把 135135 作為系數;因為 Accum(m)Accum(m) 的值是離散的(都是整數),是以我們用取整運算符,得到:
f(x)=[135x]f(x)=[135x]
我們将 xx 的值取 1,2,3……,然後觀察 f(x)f(x) 的值,可得下面這張表格:
x | 14 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
f(x) | 15 | 18 | 20 | 23 | 33 | 36 |
我們可以發現,當 x≥4x≥4 時,f(x)f(x) 的值的增幅也是 3,2,3,2,3 的循環。也就是說 f(x)f(x) 的值的增幅(x≥4x≥4 )與 Accum(m)Accum(m) 的值的增幅(m≥3m≥3 )相同,這意味着 f(x)f(x) 與 Accum(m)Accum(m) 之間相差一個常數,我們随便帶入一個具體的值計算:
f(4)−Accum(3)=10−3=7f(4)−Accum(3)=10−3=7
可知相差的常數為 7。由此可得,當 m≥3m≥3 時,Accum(m)Accum(m) 的值的序列,等于當 x≥4x≥4 時,f(x)−7f(x)−7 的值的序列。這樣我們就得到了 Accum(m),m≥3Accum(m),m≥3 的函數形式:
Accum(m)=f(m+1)−7=[13(m+1)5]−7Accum(m)=f(m+1)−7=[13(m+1)5]−7
這裡多說兩句,實際上,Accum(m)Accum(m) 的函數形式是不唯一的,使用其他的構造方法,可以得到形式不同的 Auccm(m)Auccm(m) ,隻要符合要求即可。
進一步,我們可以得到 D3D3 的函數形式:
D3=⎧⎩⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪d,31+d,(m−1)×28+[13(m+1)5]−7+d+i,m=1m=2m≥3D3={d,m=131+d,m=2(m−1)×28+[13(m+1)5]−7+d+i,m≥3
其中,平年時,i=0i=0 ;閏年時,i=1i=1 。這還不是 D3D3 最完美的形式。我們繼續分析,從 3 月份到 12 月份的 Excess 正好是兩個 3、2、3、2、3 的循環,那麼假如有第 13 月,想要繼續保持這種循環規律,13 月的 Excess 值應該是 3。而 1 月份的 Excess 的值恰好是 3,是以我們不妨變通一下,把每年的 1 月、2月當作上一年的 13月、14 月。這樣不僅仍然符合公式,而且 2 月份變成了上一年的最後一個月,也就是公式中 dd 的部分,于是平閏年的影響也去掉了,D3D3 的公式簡化成了:
D3=(m−1)×28+[13(m+1)5]−7+d,3≤m≤14D3=(m−1)×28+[13(m+1)5]−7+d,3≤m≤14
現在,我們已經得到了 D2D2 和 D3D3 的計算函數,由 D=D2+D3D=D2+D3 ,可知:
D=(y−1)×365+[y−14]−[y−1100]+[y−1400]+(m−1)×28+[13(m+1)5]−7+d,3≤m≤14D=(y−1)×365+[y−14]−[y−1100]+[y−1400]+(m−1)×28+[13(m+1)5]−7+d,3≤m≤14
注意!這個公式離正确形式還差一小步。因為在目前的公式中,每年的 1 月和 2 月被當作上一年的 13 月和 14 月計算,是以目前公式中計算閏日的部分([y−14]−[y−1100]+[y−1400][y−14]−[y−1100]+[y−1400] )存在錯誤。舉一個具體的例子,比如計算公元 4 年(閏年)3 月 1 日的星期數。在目前公式中,公元 4 年的 2 月被算作了公元 3 年的 14 月(換句話說公元 3 年變成了閏年),而公式中計算閏日的部分沒有考慮這點,依然将公元 3 年當成平年計算,是以少算了一天。是以,計算閏日的部分應當改進,公式如下:
D=(y−1)×365+[y4]−[y100]+[y400]+(m−1)×28+[13(m+1)5]−7+d,3≤m≤14(1)(1)D=(y−1)×365+[y4]−[y100]+[y400]+(m−1)×28+[13(m+1)5]−7+d,3≤m≤14
計算出 D 的值後,對 7 取模即可得到星期數。
根據同餘定理,D 除以 7 所得的餘數等于 D 式的各項分别除以 7 所得餘數之和(餘數之和大于等于 7 時,再對 7 取餘),是以可以消去 D 式中能被 7 整除的項,進行化簡:
D=(y−1)×365+[y4]−[y100]+[y400]+(m−1)×28+[13(m+1)5]−7+d=(y−1)×(364+1)+[y4]−[y100]+[y400]+[13(m+1)5]+d=(y−1)+[y4]−[y100]+[y400]+[13(m+1)5]+d(2)D=(y−1)×365+[y4]−[y100]+[y400]+(m−1)×28+[13(m+1)5]−7+d=(y−1)×(364+1)+[y4]−[y100]+[y400]+[13(m+1)5]+d(2)=(y−1)+[y4]−[y100]+[y400]+[13(m+1)5]+d
簡單說明一下:
(y−1)×365=(y−1)×(364+1)=(y−1)×364+(y−1)=(y−1)×52×7+(y−1)(y−1)×365=(y−1)×(364+1)=(y−1)×364+(y−1)=(y−1)×52×7+(y−1)
顯然,結果中的第一項是 7 的倍數,除以 7 餘數為 0,是以 (y−1)×365(y−1)×365 除以 7 的餘數其實就等于 (y−1)(y−1) 除以 7 的餘數,我們隻保留 (y−1)(y−1) 就夠了。化簡過程中,其他被銷去的項同理。
公式(2)還不是最簡練的形式,我們還可以對年份進行處理。我們現在用公式(2)計算出每個世紀第一年 3 月 1 日的星期數,得到如下結果:
年份: | 1, 401, 801, … , 2001 | 101, 501, 901, … , 2101 | 201, 601, 1001, … , 2201 | 301, 701, 1101, … ,2301 |
---|---|---|---|---|
星期: |
可以發現,每隔 4 個世紀,星期數就會重複一次。因為在數學上,-2 和 5 除以 7 的餘數相同,是以我們不妨把這個重複序列中的 5 改為 -2。這樣,4、2、0、-2 恰好構成了一個等差數列。利用等差公式,我們可以得到計算每個世紀第一年的 3 月 1 日星期數的公式:
W=4−2(cmod4)(3)(3)W=4−2(cmod4)
其中,cc 是世紀數減一。我們把公式(2)和公式(3)聯立,代入特定的日期——3 月 1 日,可以得到:
((y−1)+[y−14]−[y−1100]+[y−1400]+11)mod7=4−2(cmod4)((y−1)+[y−14]−[y−1100]+[y−1400]+11)mod7=4−2(cmod4)
利用同餘定理,經過變換得到:
(y−1)+[y−14]−[y−1100]+[y−1400]≡−2(cmod4)(mod7)(4)(4)(y−1)+[y−14]−[y−1100]+[y−1400]≡−2(cmod4)(mod7)
其中,≡≡ 是表示同餘的符号,括号中 mod7mod7 的意思是指 ≡≡ 兩邊的數除以 7 得到的餘數相同。根據公式(4),我們可以知道在每個世紀的第一年,(y−1)+[y−14]−[y−1100]+[y−1400](y−1)+[y−14]−[y−1100]+[y−1400] 可以被 −2(cmod4)−2(cmod4) 同餘替換。進而計算 DD 的公式得到如下形式:
D=−2(cmod4)+[13(m+1)5]+d(5)(5)D=−2(cmod4)+[13(m+1)5]+d
注意!現在的計算公式隻能适用于每個世紀的第一年。但是,有個這個公式,再加上計算一個世紀中閏日的部分,我們就可以很容易地得到計算這個世紀其他年份的日期的星期數的公式了。令 c 等于世紀數減一,y 等于世紀中的年份數(如 1994 年,則 c = 19,y = 94)。因為一個世紀中隻有一百年,是以不用考慮“四百年又閏”的情況;因為每百年,即每個世紀最後一年的 y = 00,而 [y4]y=0=0[y4]y=0=0 ,是以 [y4][y4] 既可以計算四年一閏的情況,又滿足百年不閏的要求 。綜合這些情況,與得到公式(2)的過程類似,我們可以得到:
D=−2(cmod4)+(y−1)+[y4]+[13(m+1)5]+d(6)(6)D=−2(cmod4)+(y−1)+[y4]+[13(m+1)5]+d
在公式(6)中,yy 是年份的後兩位。
最後,我們來把公式中的取模運算改成四則運算。設商為 qq ,餘數為 rr ,則:
4q+r=c4q+r=c
又因為,
qr=[c4]=cmod4q=[c4]r=cmod4
可得:
cmod4=c−4×[c4]cmod4=c−4×[c4]
代入公式(6)可得:
D=[c4]−2c+y−1+[y−14]+[13(m+1)5]+d(7)(7)D=[c4]−2c+y−1+[y−14]+[13(m+1)5]+d
至此,我們就得到了蔡勒公式的最終形式。
下面我們完全按自己的思路由簡單到複雜一步步進行推導……
推導之前,先作兩項規定:
①用 y, m, d, w 分别表示 年 月 日 星期(w=0-6 代表星期日-星期六
②我們從 公元0年1月1日星期日 開始
一、隻考慮最開始的 7 天,即 d = 1---7 變換到 w = 0---6
很直覺的得到:
w = d-1
二、擴充到整個1月份
模7的概念大家都知道了,也沒什麼好多說的。不過也可以從我們平常用的月曆中看出來,在周曆裡邊每列都是一個按7增長的等差數列,如1、8、15、22的星期都是相同的。是以得到整個1月的公式如下:
w = (d-1) % 7 --------- 公式⑴
三、按年擴充
由于按月擴充比較麻煩,是以将年擴充放在前面說
① 我們不考慮閏年,假設每一年都是 365 天。由于365是7的52倍多1天,是以每一年的第一天和最後一天星期是相同的。
也就是說下一年的第一天與上一年的第一天星期滞後一天。這是個重要的結論,每過一年,公式⑴會有一天的誤差,由于我們是從0年開始的,是以隻須要簡單的加上年就可以修正擴充年引起的誤差,得到公式如下:
w = (d-1 + y) % 7
② 将閏年考慮進去
每個閏年會多出一天,會使後面的年份産生一天的誤差。如我們要計算2005年1月1日星期幾,就要考慮前面的已經過的2004年中有多少個閏年,将這個誤差加上就可以正确的計算了。
根據閏年的定義(能被4整但不能被100整除或能被400整),得到計算閏年的個數的算式:y/4 - y/100 + y/400。
由于我們要計算的是目前要計算的年之前的閏年數,是以要将年減1,得到了如下的公式:
w = [d-1+y + (y-1)/4-(y-1)/100+(y-1)/400] % 7 -----公式⑵
現在,我們得到了按年擴充的公式⑵,用這個公式可以計算任一年的1月份的星期
四、擴充到其它月
考慮這個問題頗費了一翻腦筋,後來還是按前面的方法大膽假才找到突破口。
①現在我們假設每個月都是28天,且不考慮閏年
有了這個假設,計算星期就太簡單了,因為28正好是7的整數倍,每個月的星期都是一樣的,公式⑵對任一個月都适用 :)
②但假設終究是假設,首先1月就不是28天,這将會造成2月份的計算誤差。1月份比28天要多出3天,就是說公式⑵的基礎上,2月份的星期應該推後3天。
而對3月份來說,推後也是3天(2月正好28天,對3月的計算沒有影響)。
依此類推,每個月的計算要将前面幾個月的累計誤差加上。
要注意的是誤差隻影響後面月的計算,因為12月已是最後一個月,是以不用考慮12月的誤差天數,同理,1月份的誤差天數是0,因為前面沒有月份影響它。
由此,想到建立一個誤差表來修正每個月的計算。
==================================================
月 誤差 累計 模7
1 3 0 0
2 0 3 3
3 3 3 3
4 2 6 6
5 3 8 1
6 2 11 4
7 3 13 6
8 3 16 2
9 2 19 5
10 3 21 0
11 2 24 3
12 - 26 5
(閏年時2月會有一天的誤差,但我們現在不考慮)
我們将最後的誤差表用一個數組存放
在公式⑵的基礎上可以得到擴充到其它月的公式
e[] = {0,3,3,6,1,4,6,2,5,0,3,5}
w = [d-1+y + e[m-1] + (y-1)/4-(y-1)/100+(y-1)/400] % 7 --公式⑶
③上面的誤差表我們沒有考慮閏年,如果是閏年,2月會一天的誤差,會對後面的3-12月的計算産生影響,對此,我們暫時在程式設計時來修正這種情況,增加的限定條件是如果當年是閏年,且計算的月在2月以後,需要加上一天的誤差。大概代碼是這樣的:
w = (d-1 + y + e[m-1] + (y-1)/4 - (y-1)/100 + (y-1)/400);
if(m>2 && (y%4==0 && y%100!=0 || y%400==0) && y!=0)
++w;
w %= 7;
現在,已經可以正确的計算任一天的星期了。
注意:0年不是閏年,雖然現在大都不用這個條件,但我們因從公元0年開始計算,是以這個條件是不能少的。
④ 改進
公式⑶中,計算閏年數的子項 (y-1)/4-(y-1)/100+(y-1)/400 沒有包含當年,如果将當年包含進去,則實作了如果當年是閏年,w 自動加1。
由此帶來的影響是如果當年是閏年,1,2月份的計算會多一天誤差,我們同樣在程式設計時修正。則代碼如下
w = (d-1 + y + e[m-1] + y/4 - y/100 + y/400); ---- 公式⑷
if(m<3 && (y%4==0 && y%100!=0 || y%400==0) && y!=0)
--w;
與前一段代碼相比,我們簡化了 w 的計算部分。
實際上還可以進一步将常數 -1 合并到誤差表中,但我們暫時先不這樣做。
至此,我們得到了一個階段性的算法,可以計算任一天的星期了。
public class Week {
public static void main(String[] args){
int y = 2005;
int m = 4;
int d = 25;
int e[] = new int[]{0,3,3,6,1,4,6,2,5,0,3,5};
int w = (d-1+e[m-1]+y+(y>>2)-y/100+y/400);
if(m<3 && ((y&3)==0 && y%100!=0 || y%400==0) && y!=0){
--w;
}
w %= 7;
System.out.println(w);
}
}
五、簡化
現在我們推導出了自己的計算星期的算法了,但還不能稱之為公式。
所謂公式,應該給定年月日後可以手工算出星期幾的,但我們現在的算法需要記住一個誤差表才能進行計算,是以隻能稱為一種算法,還不是公式。
下面,我們試圖消掉這個誤差表……
=============================
消除閏年判斷的條件表達式
由于閏年在2月份産生的誤差,影響的是後面的月份計算。如果2月是排在一年的最後的話,它就不能對其它月份的計算産生影響了。可能已經有人聯想到了文章開頭的公式中為什麼1,2月轉換為上年的13,14月計算了吧 :)
就是這個思想了,我們也将1,2月當作上一年的13,14月來看待。
由此會産生兩個問題需要解決:
1>一年的第一天是3月1日了,我們要對 w 的計算公式重新推導
2>誤差表也發生了變化,需要得新計算
①推導 w 計算式
1> 用前面的算法算出 0年3月1日是星期3
前7天, d = 1---7 ===> w = 3----2
得到 w = (d+2) % 7
此式同樣适用于整個三月份
2> 擴充到每一年的三月份
[d + 2 + y + (y-1)/4 - (y-1)/100 + (y-1)/400] % 7
②誤差表
3 3 0 0
4 2 3 3
5 3 5 5
6 2 8 1
7 3 10 3
8 3 13 6
9 2 16 2
10 3 18 4
11 2 21 0
12 3 23 2
13 3 26 5
14 - 29 1
③得到擴充到其它月的公式
e[] = {0,3,5,1,3,6,2,4,0,2,5,1}
w = [d+2 + e[m-3] +y+(y-1)/4-(y-1)/100+(y-1)/400] % 7
(3 <= m <= 14)
我們還是将 y-1 的式子進行簡化
w = [d+2 + e[m-3] +y+y/4-y/100+y/400] % 7
這個式子如果當年是閏年,會告成多1的誤差
但我們将1,2月變換到上一年的13,14月,年份要減1,是以這個誤差會自動消除,是以得到下面的算法:
int e[] = new int[]{0,3,5,1,3,6,2,4,0,2,5,1};
if(m < 3) {
m += 12;
--y;
int w = (d+2 + e[m-3] +y+(y/4)-y/100+y/400) % 7; -----公式⑸
我們可以看到公式⑸與公式⑷幾乎是一樣的,僅僅是誤差天和一個常數的差别
常數的差別是由起始日期的星期不同引起的,0年1月1日星期日,0年3日1日星期三,有三天的差别,是以常數也從 -1 變成了 2。
現在,我們成功的消除了繁瑣的閏年條件判斷。
消除誤差表
假如存在一種m到e的函數映射關系,使得
e[m-3] = f(m)
則我們就可以用 f(m) 取代公式⑸中的子項 e[m-3],也就消除了誤差表。
由于誤差表隻有12個項,且每一項都可以加減 7n 進行調整,這個函數關系是可以拼湊出來的。但是這個過程可能是極其枯燥無味的,我現在不想自己去推導它,我要利用前人的成果。所謂前人栽樹,後人乘涼嘛 :)
文章開頭開出的公式中的 2*m+3*(m+1)/5 這個子項引起了我的興趣
經過多次試試驗,我運作下面的代碼
for(m=1; m<=14; ++m)
System.out.print((-1+2*m+3*(m+1)/5)%7 + " ");
System.out.println();
天哪,輸出結果與我的誤差表不謀而合,成功了,哈哈
2 4 0 3 5 1 3 6 2 4 0 2 5 1
Press any key to continue...
上面就是輸出結果,看它後面的12項,與我的誤差表完全吻合!!!
現在就簡單的,将 f(m) = -1 + 2*m + 3*(m+1)/5 代入公式⑸,得到
w = (d+1+2*m+3*(m+1)/5+y+(y/4)-y/100+y/400) % 7 ----公式6
限制條件: m=1,m=2 時 m=m+12,y=y-1;
現在,我們得到了通用的計算星期的公式,并且“完全”是按自己的思想推導出來的(那個函數映射關系不算),隻要了解了這個推導的步驟,即使有一天忘記了這個公式,也可以重新推導出來!
可能有人會注意到公式⑹與文章開頭的公式相差一個常數 1,這是因為原公式使用數字0--6表示星期一到星期日,而我用0--6表示星期日到星期六。實際上是一樣,你可以改成任意你喜歡的表示方法,隻需改變這個常數就可以了。
六、驗證公式的正确性。
一個月中的日期是連續的,隻要有一天對的,模7的關系就不會錯,是以一個月中隻須驗證一天就可以了,一天需要驗12天。由于擴充到年和月隻跟是否閏年有關系,就是說至少要驗證一個平年和一個閏年,也就是最少得驗證24次。
我選擇了 2005 年和 2008 年,驗證每個月的1号。
測試代碼如下:
class test {
public int GetWeek(int y, int m, int d) {
if(m<3) {
m += 12;
--y;
int w = (d+1+2*m+3*(m+1)/5+y+(y>>2)-y/100+y/400) % 7;
return w;
}
int m = 1;
int d = 1;
test t = new test();
String week[] = new String[]{
"星期日","星期一","星期二","星期三","星期四","星期五","星期六"
};
for(y=2005; y<=2008; y+=3) {
for(m=1; m<=12; ++m) {
String str = y + "-" + m + "-" + d + "\t" + week[t.GetWeek(y,m,d)];
System.out.println(str);
}
查萬年曆,檢查程式的輸出,完全正确。
七、後話
我們這個公式的推導是以0年3月1日為基礎的,對該日以後的日期都是可以計算的。但是否可以擴充到公元前(1,2已屬于公元前1年的13,14月了)呢?
雖然我對0年1月和2月、以及公元前1年(令y=-1)的12月作了驗證是正确的,但我在推導這個公式時并未想到将其擴充到公元前,是以上面的推導過程沒有足夠理論依據可以證明其适用于公元前。(負數的取模在不同的編譯器如C++中好象處理并不完全正确)。
另外一有點是對于0年是否存在的争議,一種折中的說法是0年存在,但什麼也沒有發生,其持續時間為0。還有在羅馬的格利戈裡曆法中有10天是不存的(1582年10月5日至14持續時間為0),英國的曆法中有11天(1752年9月3日至13日)是不存在的。感興趣的朋友可以看看這裡:
http://www.whtv.com.cn/zhuanti/celebration/when/wz16.htm
但是我們做的是數字計算,不管那一天是否存在,持續的時間是24小時還是23小時甚至是0小時,隻要那個号碼存在,就有一個星期與之對應。是以這個公式仍然是适用的。
如果要計算的是時間段,就必須考慮這個問題了。
