2/29
今年閏年,今天閏日,并且又趕上周末,寫一篇與技術(shù)不太相關(guān)的小文章吧。
回歸陽歷與閏年
由于地球的地軸與公轉(zhuǎn)軌道平面不垂直(即存在黃赤交角),因此在公轉(zhuǎn)的過程中,地球上的太陽直射點會周期性移動,即北半球夏至?xí)r陽光直射北回歸線,北半球冬至?xí)r陽光直射南回歸線,而春分、秋分時直射赤道,如下圖所示。
根據(jù)太陽直射點周期性移動制定的歷法稱為回歸陽歷(tropical solar calendar),一個周期的長度就稱為1回歸年(tropical year)。在2000年時測定的1回歸年長度為365天5小時48分45.19秒——即約365.2421897天。
回歸陽歷的代表就是格里歷(Gregorian calendar),即我們平時所說的“公歷”、“公元”。它由教皇額我略十三世(Gregory XIII)于1582年頒行,并首先在意大利、西班牙等地使用,進(jìn)而影響到全世界,1912年被中國采用。
格里歷(以及絕大多數(shù)回歸陽歷)的平年有365天,比回歸年少了1/4天不到。為了彌補(bǔ)人為歷法與實際回歸年之間的時間差,格里歷的制訂者規(guī)定每4年修正一次,即在大多數(shù)4的倍數(shù)年的2月加上一天——即2月29日,稱為閏日(leap day),對應(yīng)的那一年就是閏年(leap year)。
為什么是“大多數(shù)”4的倍數(shù)年,而不是“全部”?很顯然,如果以4個世紀(jì)為區(qū)間,所有4的倍數(shù)年都加上一天的話,那么這400年又會累積出大約3天的誤差。為了把這多出來的3天抹平,格里歷又規(guī)定在整世紀(jì)年中,只有400的倍數(shù)年為閏年(如1600、2000),其他的都是平年(如1700、1800、1900)。
所以,格里歷中正確判斷閏年的方法如下。
boolean isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
可以計算得知,格里歷的年平均長度為365 + ?1/4 ? ?1/100 + ?1/400 = 365.2425天,與回歸年的標(biāo)準(zhǔn)長度相當(dāng)接近,每約3300年才會又差出1天。至于這個誤差如何修正,就留給后人去解決吧。
下面這張圖表又示出了1800~2200年這四個世紀(jì)的時間中,由于閏年和閏日的影響,北半球夏至實際日期在公歷6月20、21、22日區(qū)間內(nèi)的波動。
公歷的閏年bug
閏年bug總體上講沒有千年蟲那么嚴(yán)重,但是也會造成困擾。比如2012年時,微軟的Azure服務(wù)因為閏年bug導(dǎo)致整體下線(詳情請見這里),而于此同時,谷歌的Gmail聊天歷史中也因為閏年bug將所有2月29日的記錄標(biāo)成了1969年12月31日。直到今天,Excel仍然會錯誤地認(rèn)為1900年是個閏年(輸入1900年2月29日,會被Excel認(rèn)為是合法的),這個問題很久遠(yuǎn)了,詳情可以參見這里。
在程序設(shè)計語言中做基于年的運算,也會出現(xiàn)小型的閏年bug,例如C#:
DateTime dt = DateTime.Now;
DateTime result = new DateTime(dt.Year + 1, dt.Month, dt.Day);
在今天執(zhí)行這條語句,會直接拋出ArgumentOutOfRangeException,因為2021年是平年,沒有2月29日。
又例如JS:
var dt = new Date();
dt.setFullYear(dt.getFullYear() + 1);
dt的值會變成2021年3月1日,因為JS檢測到2021年2月29日不存在,就會自動置入下一個有效的日期。但是,如果以正常的一年365天計,結(jié)果應(yīng)該是2021年2月28日才正確。
恒星陽歷
與回歸陽歷相對的陽歷歷法是恒星陽歷(sidereal solar calendar),顧名思義,它是以太陽、地球與某一恒星的相對位置來確定的歷法。從地球上看,太陽從黃道上與某恒星共線的位置出發(fā),再次回到與該恒星共線的位置的周期即為1恒星年(sidereal year)。1恒星年也就是地球?qū)嶋H上的公轉(zhuǎn)周期,計365天6小時9分9.76秒——即約365.256363004天。
采用恒星陽歷的歷法不多,主要分布在南亞次大陸,如印度歷。由于相同的誤差原因,恒星陽歷也會有閏年,不再多講。
陰陽歷與閏月
陰陽歷(lunisolar calendar)是另一種主流的歷法。陰陽歷中的一年以地球公轉(zhuǎn)周期——即回歸年或恒星年來定義,但是月份和日期以月球公轉(zhuǎn)周期——即朔望月來定義。陰陽歷法在東亞地區(qū)有很悠久的歷史,最具代表性的就是中國農(nóng)歷,日本、韓國、越南等國現(xiàn)今也仍然采用中國農(nóng)歷與其變種。
需要注意,雖然民間經(jīng)常將農(nóng)歷叫做“陰歷”,但嚴(yán)格來講它是陰陽歷。真正的“陰歷”是不考慮地球公轉(zhuǎn)周期的,如伊斯蘭歷,就不贅述了。
中國農(nóng)歷的“陽”,體現(xiàn)在二十四節(jié)氣。古人根據(jù)太陽的視運動與自然氣象的變化,將太陽沿黃經(jīng)每運行15度所經(jīng)歷的時日稱為一個節(jié)氣,以指導(dǎo)農(nóng)業(yè)生產(chǎn)。太陽運行360度為1回歸年,正好是24個節(jié)氣。所以二十四節(jié)氣恰好能平分給公歷的12個月份,并且日期也比較固定,上半年在6日、21日左右,下半年在8日、23日左右,正如《新華字典》附錄的節(jié)氣歌所述:
春雨驚春清谷天,夏滿芒夏暑相連。
秋處露秋寒霜降,冬雪雪冬小大寒。
每月兩節(jié)不變更,最多相差一兩天。
上半年來六廿一,下半年是八廿三。
中國農(nóng)歷的“陰”,則體現(xiàn)在以月相定義月份。具體來說,是以月球合朔(即月亮陰面朝向地球)發(fā)生的那一天為該月的初一日,月圓(即“望”)的那一天則是十五日。月球兩次合朔經(jīng)過的周期就是朔望月,即農(nóng)歷的一個月。
由于攝動的因素,朔望月的長度大約在29.27至29.83天之間變動,而長時期的平均長度大約為29.530588天(29天12小時44分2.8秒)。所以農(nóng)歷中會有大小月之分,大月30日,小月29日,嚴(yán)格按照天文觀測來確定——這點與公歷不同,公歷每個月的天數(shù)完全是人為規(guī)定的。
一個農(nóng)歷年的平年由12個月組成,即大約29.5 * 12 = 354日,但一個回歸年大約是365又1/4日,差了大約11天。這個誤差是比較大的,積累十幾年就會出現(xiàn)月份與季節(jié)氣候完全對不上的情況,所以古人才發(fā)明了“閏月”,以協(xié)調(diào)回歸年與農(nóng)歷年。
那么什么時候需要插入閏月呢?決定閏月的算法稱為“無中置閏法”,早在《太初歷》(公元前104年)就已經(jīng)有了。《后漢書·律歷下》有言:
...故置十二中以定月位。有朔而無中者為閏月。中之始曰節(jié),與中為二十四氣。
該算法確立了一個大前提,即農(nóng)歷年起于冬至,終于冬至(所以冬至也是一個重要的節(jié)日),這樣就把回歸年和農(nóng)歷年的長度統(tǒng)一起來了。
接下來,檢查一年中兩個冬至之間有幾個朔望月。如果有12個,說明是正常的,不需要加閏月。如果有13個,那么就從農(nóng)歷二月到十月間,將第一個沒有“中氣”的月定為閏月,這個月跟在哪個正常的月份后面就是閏幾月。所謂中氣,其實就是指太陽運行到30度角倍數(shù)的12個節(jié)氣,即雨水、春分、谷雨、小滿、夏至、大暑、處暑、秋分、霜降、小雪、冬至、大寒。
為什么會有月份沒有中氣?因為地球的軌道并不是圓的,而是近似橢圓,所以在遠(yuǎn)日點附近(北半球夏季左右)公轉(zhuǎn)較慢,導(dǎo)致兩個中氣之間的間隔(平均約30.43日,最長可達(dá)31.45日)被拉長,長于朔望月的長度,所以會有月份恰好卡在了兩個中氣之間。舉個例子,2001年5月21日(四月二十九)是小滿節(jié)氣,下一個朔望月的區(qū)間是5月23日到6月20日,但下一個中氣——即夏至——卻是6月21日。所以這個朔望月沒有中氣,從5月23日起即為閏四月初一,直到6月20日為閏四月廿九,從6月21日才算五月。
The End
從沒寫過這樣的東西,有點意思。
民那晚安。