推導基姆拉爾森公式根據日期計算星期


轉自:http://topic.csdn.net/t/20050425/23/3966336.html

======================================================== 
        計算給定日期星期幾好象是編程都會遇到的問題,最近論壇里也有人提到這個問題,並給出了一個公式: 
        W=   (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400)   mod   7 
        (要求將1、2月當作上一年的13、14月來計算) 

        去看了看這個公式的原帖                         http://blog.csdn.net/ycrao/archive/2000/11/24/3825.aspx 
        其講述的過程並不清楚,便想怎樣自己推導出一個公式來,花了幾個小時,總算是弄出來了,結果跟上面的公式一樣:) 
======================================================== 

下面我們完全按自己的思路由簡單到復雜一步步進行推導…… 

推導之前,先作兩項規定: 
①用   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   %=   7; 
        
        與前一段代碼相比,我們簡化了   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 

        ②誤差表 
================================================== 
月     誤差   累計     模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 
        (3   <=   m   <=   14) 

        這個式子如果當年是閏年,會告成多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; 
        } 
} 

public   class   Week   { 
        public   static   void   main(String[]   args){ 
                int   y   =   2005; 
                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小時,只要那個號碼存在,就有一個星期與之對應。所以這個公式仍然是適用的。 
        如果要計算的是時間段,就必須考慮這個問題了。 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM