2/29
今年閏年,今天閏日,并且又趕上周末,寫一篇與技術(shù)不太相關(guān)的小文章吧。
回歸陽(yáng)歷與閏年
由于地球的地軸與公轉(zhuǎn)軌道平面不垂直(即存在黃赤交角),因此在公轉(zhuǎn)的過(guò)程中,地球上的太陽(yáng)直射點(diǎn)會(huì)周期性移動(dòng),即北半球夏至?xí)r陽(yáng)光直射北回歸線,北半球冬至?xí)r陽(yáng)光直射南回歸線,而春分、秋分時(shí)直射赤道,如下圖所示。

根據(jù)太陽(yáng)直射點(diǎn)周期性移動(dòng)制定的歷法稱為回歸陽(yáng)歷(tropical solar calendar),一個(gè)周期的長(zhǎng)度就稱為1回歸年(tropical year)。在2000年時(shí)測(cè)定的1回歸年長(zhǎng)度為365天5小時(shí)48分45.19秒——即約365.2421897天。
回歸陽(yáng)歷的代表就是格里歷(Gregorian calendar),即我們平時(shí)所說(shuō)的“公歷”、“公元”。它由教皇額我略十三世(Gregory XIII)于1582年頒行,并首先在意大利、西班牙等地使用,進(jìn)而影響到全世界,1912年被中國(guó)采用。
格里歷(以及絕大多數(shù)回歸陽(yáng)歷)的平年有365天,比回歸年少了1/4天不到。為了彌補(bǔ)人為歷法與實(shí)際回歸年之間的時(shí)間差,格里歷的制訂者規(guī)定每4年修正一次,即在大多數(shù)4的倍數(shù)年的2月加上一天——即2月29日,稱為閏日(leap day),對(duì)應(yīng)的那一年就是閏年(leap year)。
為什么是“大多數(shù)”4的倍數(shù)年,而不是“全部”?很顯然,如果以4個(gè)世紀(jì)為區(qū)間,所有4的倍數(shù)年都加上一天的話,那么這400年又會(huì)累積出大約3天的誤差。為了把這多出來(lái)的3天抹平,格里歷又規(guī)定在整世紀(jì)年中,只有400的倍數(shù)年為閏年(如1600、2000),其他的都是平年(如1700、1800、1900)。
所以,格里歷中正確判斷閏年的方法如下。
boolean isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
可以計(jì)算得知,格里歷的年平均長(zhǎng)度為365 + ?1/4 ? ?1/100 + ?1/400 = 365.2425天,與回歸年的標(biāo)準(zhǔn)長(zhǎng)度相當(dāng)接近,每約3300年才會(huì)又差出1天。至于這個(gè)誤差如何修正,就留給后人去解決吧。
下面這張圖表又示出了1800~2200年這四個(gè)世紀(jì)的時(shí)間中,由于閏年和閏日的影響,北半球夏至實(shí)際日期在公歷6月20、21、22日區(qū)間內(nèi)的波動(dòng)。

公歷的閏年bug
閏年bug總體上講沒有千年蟲那么嚴(yán)重,但是也會(huì)造成困擾。比如2012年時(shí),微軟的Azure服務(wù)因?yàn)殚c年bug導(dǎo)致整體下線(詳情請(qǐng)見這里),而于此同時(shí),谷歌的Gmail聊天歷史中也因?yàn)殚c年bug將所有2月29日的記錄標(biāo)成了1969年12月31日。直到今天,Excel仍然會(huì)錯(cuò)誤地認(rèn)為1900年是個(gè)閏年(輸入1900年2月29日,會(huì)被Excel認(rèn)為是合法的),這個(gè)問(wèn)題很久遠(yuǎn)了,詳情可以參見這里。
在程序設(shè)計(jì)語(yǔ)言中做基于年的運(yùn)算,也會(huì)出現(xiàn)小型的閏年bug,例如C#:
DateTime dt = DateTime.Now;
DateTime result = new DateTime(dt.Year + 1, dt.Month, dt.Day);
在今天執(zhí)行這條語(yǔ)句,會(huì)直接拋出ArgumentOutOfRangeException,因?yàn)?021年是平年,沒有2月29日。
又例如JS:
var dt = new Date();
dt.setFullYear(dt.getFullYear() + 1);
dt的值會(huì)變成2021年3月1日,因?yàn)镴S檢測(cè)到2021年2月29日不存在,就會(huì)自動(dòng)置入下一個(gè)有效的日期。但是,如果以正常的一年365天計(jì),結(jié)果應(yīng)該是2021年2月28日才正確。
恒星陽(yáng)歷
與回歸陽(yáng)歷相對(duì)的陽(yáng)歷歷法是恒星陽(yáng)歷(sidereal solar calendar),顧名思義,它是以太陽(yáng)、地球與某一恒星的相對(duì)位置來(lái)確定的歷法。從地球上看,太陽(yáng)從黃道上與某恒星共線的位置出發(fā),再次回到與該恒星共線的位置的周期即為1恒星年(sidereal year)。1恒星年也就是地球?qū)嶋H上的公轉(zhuǎn)周期,計(jì)365天6小時(shí)9分9.76秒——即約365.256363004天。
采用恒星陽(yáng)歷的歷法不多,主要分布在南亞次大陸,如印度歷。由于相同的誤差原因,恒星陽(yáng)歷也會(huì)有閏年,不再多講。
陰陽(yáng)歷與閏月
陰陽(yáng)歷(lunisolar calendar)是另一種主流的歷法。陰陽(yáng)歷中的一年以地球公轉(zhuǎn)周期——即回歸年或恒星年來(lái)定義,但是月份和日期以月球公轉(zhuǎn)周期——即朔望月來(lái)定義。陰陽(yáng)歷法在東亞地區(qū)有很悠久的歷史,最具代表性的就是中國(guó)農(nóng)歷,日本、韓國(guó)、越南等國(guó)現(xiàn)今也仍然采用中國(guó)農(nóng)歷與其變種。
需要注意,雖然民間經(jīng)常將農(nóng)歷叫做“陰歷”,但嚴(yán)格來(lái)講它是陰陽(yáng)歷。真正的“陰歷”是不考慮地球公轉(zhuǎn)周期的,如伊斯蘭歷,就不贅述了。
中國(guó)農(nóng)歷的“陽(yáng)”,體現(xiàn)在二十四節(jié)氣。古人根據(jù)太陽(yáng)的視運(yùn)動(dòng)與自然氣象的變化,將太陽(yáng)沿黃經(jīng)每運(yùn)行15度所經(jīng)歷的時(shí)日稱為一個(gè)節(jié)氣,以指導(dǎo)農(nóng)業(yè)生產(chǎn)。太陽(yáng)運(yùn)行360度為1回歸年,正好是24個(gè)節(jié)氣。所以二十四節(jié)氣恰好能平分給公歷的12個(gè)月份,并且日期也比較固定,上半年在6日、21日左右,下半年在8日、23日左右,正如《新華字典》附錄的節(jié)氣歌所述:
春雨驚春清谷天,夏滿芒夏暑相連。
秋處露秋寒霜降,冬雪雪冬小大寒。
每月兩節(jié)不變更,最多相差一兩天。
上半年來(lái)六廿一,下半年是八廿三。
中國(guó)農(nóng)歷的“陰”,則體現(xiàn)在以月相定義月份。具體來(lái)說(shuō),是以月球合朔(即月亮陰面朝向地球)發(fā)生的那一天為該月的初一日,月圓(即“望”)的那一天則是十五日。月球兩次合朔經(jīng)過(guò)的周期就是朔望月,即農(nóng)歷的一個(gè)月。

由于攝動(dòng)的因素,朔望月的長(zhǎng)度大約在29.27至29.83天之間變動(dòng),而長(zhǎng)時(shí)期的平均長(zhǎng)度大約為29.530588天(29天12小時(shí)44分2.8秒)。所以農(nóng)歷中會(huì)有大小月之分,大月30日,小月29日,嚴(yán)格按照天文觀測(cè)來(lái)確定——這點(diǎn)與公歷不同,公歷每個(gè)月的天數(shù)完全是人為規(guī)定的。
一個(gè)農(nóng)歷年的平年由12個(gè)月組成,即大約29.5 * 12 = 354日,但一個(gè)回歸年大約是365又1/4日,差了大約11天。這個(gè)誤差是比較大的,積累十幾年就會(huì)出現(xiàn)月份與季節(jié)氣候完全對(duì)不上的情況,所以古人才發(fā)明了“閏月”,以協(xié)調(diào)回歸年與農(nóng)歷年。
那么什么時(shí)候需要插入閏月呢?決定閏月的算法稱為“無(wú)中置閏法”,早在《太初歷》(公元前104年)就已經(jīng)有了?!逗鬂h書·律歷下》有言:
...故置十二中以定月位。有朔而無(wú)中者為閏月。中之始曰節(jié),與中為二十四氣。
該算法確立了一個(gè)大前提,即農(nóng)歷年起于冬至,終于冬至(所以冬至也是一個(gè)重要的節(jié)日),這樣就把回歸年和農(nóng)歷年的長(zhǎng)度統(tǒng)一起來(lái)了。
接下來(lái),檢查一年中兩個(gè)冬至之間有幾個(gè)朔望月。如果有12個(gè),說(shuō)明是正常的,不需要加閏月。如果有13個(gè),那么就從農(nóng)歷二月到十月間,將第一個(gè)沒有“中氣”的月定為閏月,這個(gè)月跟在哪個(gè)正常的月份后面就是閏幾月。所謂中氣,其實(shí)就是指太陽(yáng)運(yùn)行到30度角倍數(shù)的12個(gè)節(jié)氣,即雨水、春分、谷雨、小滿、夏至、大暑、處暑、秋分、霜降、小雪、冬至、大寒。
為什么會(huì)有月份沒有中氣?因?yàn)榈厍虻能壍啦⒉皇菆A的,而是近似橢圓,所以在遠(yuǎn)日點(diǎn)附近(北半球夏季左右)公轉(zhuǎn)較慢,導(dǎo)致兩個(gè)中氣之間的間隔(平均約30.43日,最長(zhǎng)可達(dá)31.45日)被拉長(zhǎng),長(zhǎng)于朔望月的長(zhǎng)度,所以會(huì)有月份恰好卡在了兩個(gè)中氣之間。舉個(gè)例子,2001年5月21日(四月二十九)是小滿節(jié)氣,下一個(gè)朔望月的區(qū)間是5月23日到6月20日,但下一個(gè)中氣——即夏至——卻是6月21日。所以這個(gè)朔望月沒有中氣,從5月23日起即為閏四月初一,直到6月20日為閏四月廿九,從6月21日才算五月。
The End
從沒寫過(guò)這樣的東西,有點(diǎn)意思。
民那晚安。