演算法系列之二十:計算中國農曆(二)

所謂的「天文演算法」,就是利用經典力學定律推導行星運轉軌道,對任意時刻的行星位置進行精確計算,從而獲得某種天文現象發生時的時間,比如日月合朔這一天文現象就是太陽和月亮的地心黃經(視黃經)差為0的那一瞬間。能夠計算任意時刻行星位置的一套理論就被稱為星曆表,比較著名的星曆表有美國國家航空航天局下屬的噴氣推進實驗室發布的DE系列星曆表,還有瑞士天文台在DE406基礎上拓展的瑞士星曆表等等。根據行星運行軌道直接計算行星位置通常不是很方便,更何況大多數民用天文計算用不上那麼多精確的軌道參數,於是天文學家在這些星曆表的基礎上推導出了很多可以做簡便計算,但是又能保證一定精度的行星運行理論,比較著名的有VSOP82/87太陽系行星運行理論和ELP-2000/82月球運行理論,這兩套理論在精度上已經很接近DE系列星曆表了。關於如何應用這兩套倫理進行天文曆法計算,請參考「日曆生成演算法」系列文章的第三篇《用天文方法計算二十四節氣》和第四篇《用天文方法計算日月合朔》,本文介紹的農曆年曆推算是在已經通過天文演算法獲得了精確的節氣時間和日月合朔時間的基礎上進行的。中國的官方紀時採用的是中國公曆(格里曆),因此農曆年曆的推導應以公曆年的周期為主導,附上農曆年的信息,也就是說,年曆以公曆的1月1日為起始,至12月31日結束,根據農曆曆法推導出的農曆日期信息,附加在公曆日期信息上形成雙歷。通常情況下,一個公曆年周期都不能完整地對應到一個農曆年周期上,二者的偏差也不固定,因此不存在穩定的對應關係,也就是說,不存在從公曆的日期到農曆日期的轉換公式,只能根據農曆的曆法規則推導出農曆日期與公曆日期的對應關係。由農曆曆法規則可知,上一個公曆年的冬至()所在的朔望月是上一個農曆年的十一月(冬月),所以在進行節氣計算時,需要計算包括上一年冬至節氣在內的二十五個節氣,才能對應上上一個農曆年的十一月和當前農曆年的十一月。在計算與之對應的朔日時,考慮到有閏月的情況,需要從上一年冬至節氣前的第一個朔日,連續計算15個朔日才能保證覆蓋兩個冬至之間的一整年時間,圖(1)顯示了2011年沒有閏月的情況下朔日和冬至的關係:

圖(1)沒有閏月情況下朔日與冬至節氣關係圖圖中上排數字是公曆月的編號,黑色圓點代表朔日,黑色三角形代表冬至節氣。圖(2)顯示了2012年有閏月的情況下朔日和冬至的關係:

圖(2)有閏月情況下朔日與冬至節氣關係圖通過計算得到能夠覆蓋兩個冬至節氣的所有朔日時間後,就可以著手建立公曆日期與農曆日期的對應關係。以圖(1)所示的2011年為例,首先根據計算得到的15個朔日(2011年只會用到其中的前14個時間)時間,建立與2011年(公曆年)有關的朔望月關係表:朔日編號合朔時間對應公曆日期月長月名101:35:39.902010-12-0629冬月217:02:34.262011-01-0430臘月310:30:42.672011-02-0330正月404:45:59.442011-03-0529二月522:32:15.132011-04-0330三月614:50:31.792011-05-0330四月705:02:32.512011-06-0229五月816:53:54.102011-07-0130六月902:39:45.062011-07-3129七月1011:04:06.432011-08-2929八月1119:08:50.092011-09-2730九月1203:55:54.642011-10-2729十月1314:09:40.972011-11-2530冬月1402:06:27.052011-12-2529臘月1515:39:23.992012-01-2330正月表(2)2011年朔望月與公曆日期關係表編號為1和2的兩個朔日之間的朔望月是十一月,因為冬至節氣落在這個朔望月,其它月的月名依次類推,正月的朔日就是春節。輸出公曆和農曆雙歷時,以月(公曆)為單位,從每月第一天開始,依次判斷每一天屬於哪個朔望月,確定這一天的農曆月名,然後比較這一天和這個朔望月的朔日之間相差幾天,記為農曆日期。以2011年1月1日為例,這一天在2010年12月6日(2010年農曆十一月的朔日)和2011年1月4日之間(2010年農曆十二月的朔日),查表(1)可知對應的農曆月是十一月,這一天和2010年12月6日相差26天,因此這一天的農曆日期就是「廿七」。再以2011年2月3日(春節)這一天為例,查朔望月表得知2月3日屬於從2月3日開始的朔望月,這個朔望月的月名是正月,而2月3日就是月首,農曆日期是初一,正月初一就是春節。先來介紹兩個函數,這兩個函數分別用於計算節氣和日月合朔發生的時間,函數演算法的具體描述將在「日曆生成演算法」系列文章的第三篇《用天文方法計算二十四節氣》和第四篇《用天文方法計算日月合朔》中介紹,此處只是簡單介紹一下用法。首先是計算節氣時間的函數:5 double CalculateSolarTerms(int year, int angle);這個函數用於計算指定的年份(year參數)中,太陽在黃道上運行(視運動)到指定角度時的時間,angle可以設定節氣發生時的角度,比如CalculateSolarTerms(2011, 270)就是計算2011年冬至的時間。這個函數返回的時間類型是儒略日,關於儒略日的說明請參考「日曆生成演算法」系列文章的第一篇《中國公曆(格里曆)》。接下來介紹計算日月合朔時間的函數:8 double CalculateMoonShuoJD(double tdJD);這個函數返回指定時間附近的朔日時間,搜索的範圍是tdJD參數指定時間的前一天到後29.5305天,tdJD參數和返回值的時間類型都是儒略日。生成指定公曆年份的公曆和農曆的雙歷年曆的流程如下:

圖(3)計算公農曆雙歷年曆的流程GetAllSolarTermsJD()函數從指定年份的指定節氣開始,連續計算25個節氣時間,時間可以跨年份,內部判斷過冬至節氣後自動轉到下一年的節氣繼續計算:139 void CChineseCalendar::GetAllSolarTermsJD(int year, int start, double *SolarTerms)140 {141     int i = 0;142     int st = start;143     while(i < 25)144     {145         double jd = CalculateSolarTerms(year, st * 15);147         if(st == WINTER_SOLSTICE)148         {149             year++;150         }151         st = (st + 1) % SOLAR_TERMS_COUNT;152     }153 }start參數是節氣的索引,定義二十四節氣的索引如下:38 const int VERNAL_EQUINOX      = 0;    // 春分39 const int CLEAR_AND_BRIGHT    = 1;    // 清明40 const int GRAIN_RAIN          = 2;    // 穀雨41 const int SUMMER_BEGINS       = 3;    // 立夏42 const int GRAIN_BUDS          = 4;    // 小滿43 const int GRAIN_IN_EAR        = 5;    // 芒種44 const int SUMMER_SOLSTICE     = 6;    // 夏至45 const int SLIGHT_HEAT         = 7;    // 小暑46 const int GREAT_HEAT          = 8;    // 大暑47 const int AUTUMN_BEGINS       = 9;    // 立秋48 const int STOPPING_THE_HEAT   = 10;   // 處暑49 const int WHITE_DEWS          = 11;   // 白露50 const int AUTUMN_EQUINOX      = 12;   // 秋分51 const int COLD_DEWS           = 13;   // 寒露52 const int HOAR_FROST_FALLS    = 14;   // 霜降53 const int WINTER_BEGINS       = 15;   // 立冬54 const int LIGHT_SNOW          = 16;   // 小雪55 const int HEAVY_SNOW          = 17;   // 大雪56 const int WINTER_SOLSTICE     = 18;   // 冬至57 const int SLIGHT_COLD         = 19;   // 小寒58 const int GREAT_COLD          = 20;   // 大寒59 const int SPRING_BEGINS       = 21;   // 立春60 const int THE_RAINS           = 22;   // 雨水61 const int INSECTS_AWAKEN      = 23;   // 驚蟄節氣索引乘以15就是節氣在黃道上對應的度數。GetNewMoonJDs()函數從指定時間開始連續計算15個朔日時間,從第一個冬至節氣前的第一個朔日開始。15個朔日可以形成14個完整的朔望月,保證在有閏月的情況下也能包含兩個冬至節氣:137 void CChineseCalendar::GetNewMoonJDs(double jd, double *NewMoon)138 {139     for(int i = 0; i < NEW_MOON_CALC_COUNT; i++)140     {141         double shuoJD = CalculateMoonShuoJD(jd);142         NewMoon[i] = shuoJD;143144         jd += 29.5; /*轉到下一個最接近朔日的時間*/145     }146 }BuildAllChnMonthInfo()函數根據15個朔日時間組成14個朔望月,根據相鄰朔日的間隔計算出農曆月天數用來判定大小月,並且從「十一月」開始依次為每個朔望月命名(月建名稱):170 bool CChineseCalendar::BuildAllChnMonthInfo()171 {172     CHN_MONTH_INFO info; //一年最多可13個農曆月173     int i;174     int yuejian = 11;   //採用夏曆建寅,冬至所在月份為農曆11月175     for(i = 0; i < (NEW_MOON_CALC_COUNT - 1); i++)176     {177         info.mmonth = i;178         info.mname = (yuejian <= 12) ? yuejian : yuejian - 12;179         info.shuoJD = m_NewMoonJD[i];180         info.nextJD = m_NewMoonJD[i + 1];181         info.mdays = int(info.nextJD + 0.5) - int(info.shuoJD + 0.5);182         info.leap = 0;183184         CChnMonthInfo cm(&info);185         m_ChnMonthInfo.push_back(cm);186187         yuejian++;188     }189190     return (m_ChnMonthInfo.size() == (NEW_MOON_CALC_COUNT - 1));191 }CalcLeapChnMonth()函數根據節氣和朔日時間判斷在兩個冬至節氣之間的農曆年是否有閏月,判斷的依據就是看第十四個朔日是否在第二個冬至節氣之前,如果第十四個朔日發生在第二個冬至節氣之前,就說明在兩個冬至節氣之間發生了十三次朔日,需要置閏月。因為農曆中十二個中氣屬於哪個農曆月是固定的,因此置閏月的過程就是依次判斷十二個中氣是否在對應的農曆月中,如果本應該屬於某個農曆月的中氣卻沒有落在這個農曆月中,則這個農曆月就是閏月,需要設置閏月標誌,同時調整這個月之後的月名。調整農曆月名的方法就是月名減一,比如原來是八月就要調整為七月,這樣就將十三個月對應上了十二個月名(其中多出來的一個農曆月被命名為閏某月)。如果節氣和朔日發生在同一天,CalcLeapChnMonth()函數採用的是民間曆法的規則,與現行曆法一致:194 void CChineseCalendar::CalcLeapChnMonth()195 {196     assert(m_ChnMonthInfo.size() > 0); /*陰曆月的初始化必須在這個之前*/197198     int i;199200     if(int(m_NewMoonJD[13] + 0.5) <= int(m_SolarTermsJD[24] + 0.5)) //第13月的月末沒有超過冬至,說明今年需要閏一個月201     {202         //找到第一個沒有中氣的月203         i = 1;204         while(i < (NEW_MOON_CALC_COUNT - 1))205         {206207             /*m_NewMoonJD[i + 1]是第i農曆月的下一個月的月首,本該屬於第i月的中氣如果比下一個月208               的月首還晚,或者與下個月的月首是同一天(民間曆法),則說明第i月沒有中氣*/209             if(int(m_NewMoonJD[i + 1] + 0.5) <= int(m_SolarTermsJD[2 * i] + 0.5))210                 break;211             i++;212         }213         if(i < (NEW_MOON_CALC_COUNT - 1)) /*找到閏月,對後面的農曆月調整月名*/214         {215             m_ChnMonthInfo[i].SetLeapMonth(true);216             while(i < (NEW_MOON_CALC_COUNT - 1))217             {218                 m_ChnMonthInfo[i++].ReIndexMonthName();219             }220         }221     }222 }從理論上講,本文介紹的演算法在精度允許的範圍內可以計算前後幾千年的農曆年曆,但是對古代的農曆計算需要小心。首先是「平朔」和「定朔」的問題,唐代以前使用的是平朔方法定月首,本文介紹的計算方法採用的是「定朔」方法,因此計算出的年曆與唐代以前的歷史會不一致。另外,即是在唐代以後採用「定朔」的曆法,因為古代天文觀測和計算受條件限制,可能不夠精確,因此與現在用天文演算法計算出的結果可能並不一致。所以對歷史農曆的計算應該以歷史事實為主,天文計算為輔,當計算與歷史不一致時,要根據歷史數據進行校正。Calendar.exe是根據本文介紹的演算法編寫的日曆小程序,沒有太多的功能,主要是為了驗證演算法,因為沒有歷史數據用於修正結果,因此不支持1601年以前的農曆計算(也就是說按照天文演算法計算出來的結果可能和實際歷史上的曆法不符)。

圖(5)演示程序的界面小知識1:民間曆法和歷理曆法新中國成立以後沒有頒布新的「官方農曆曆法」,將曆法和政治分離體現了時代的進步,但是由於沒有 「官方曆法」,也引起了一些問題。比如我國現在採用的農曆曆法是《時憲歷》,它源於清朝順治年間(公元1645)頒布的《順治歷》,它有兩個不足之處:一個是日月合朔和節氣的時間以北京當地時間為準,也就是東經116度25分的當地時間,其節氣和新月的觀察只適用於中原地區。其它經度的地方,因為時間的關係,對導致日月合朔和節氣時間的差異導致置閏和月順序各不相同。另一個不足之處就是日月合朔時間和節氣時間判斷不精確,如果日月合朔時間和節氣時間在同一天,不管具體的時間是否有先後,一律將此節氣算做新月中的節氣,這樣一來,如果這個節氣是中氣,就會影響到閏月的設置。歷理曆法針對這兩點進行了改進,對節氣時間和日月合朔時間統一採用東經120度即東八區標準時,這樣在任何時區的節氣和置閏結果都是一樣的,以東八區標準時為準。對於節氣時間和日月合朔時間在同一天的情況,精確計算到時、分、秒,只有日月合朔時間在節氣時間之前,這個節氣才包含在次月內。歷理曆法從理論上講更符合現代天文學的精確計算,但是需要注意的是,歷理曆法仍然只是存在於理論上的曆法,我國現行的農曆曆法依然是民間曆法《時憲歷》或《順治歷》。小知識2:通式壽星公式「通式壽星公式」是前人整理出來的一個用於計算每年立春日期的經驗公式:Date =  - L其中,Y是年份,D的值是0.2422,C是經驗值,取決於節氣和年份,對於21世紀,立春節氣的C值是4.475,春分節氣的C值是20.646等等;L是閏年數,其計算公式為:L =  -  +用「通式壽星公式」確定2011年立春日期的過程如下:L = int(2011/4) – int(2011/100) + int(2011/400) = 502 – 20 + 5 = 487Date = int(2011×0.2422+4.475)- 487 = 491 – 487 = 4所以,2011年的立春日期是2月4日。小知識3:計算節氣和朔日的經驗公式以1900年1月0日(星期日)為基準日,之後的每一天與基準日的差值稱為「積日」, 1900年1月1日的積日是1,以後的時間依次類推,則計算第y年第x個節氣的積日公式是:F = 365.242 * (y – 1900) + 6.2 + 15.22 *x - 1.9 * sin(0.262 * x)其中x是節氣的索引,0代表小寒,1代表大寒,其它節氣按照順序類推。計算從1900年開始第m個朔日的公式是:M = 1.6 + 29.5306 * m + 0.4 * sin(1 - 0.45058 * m)小知識4:平朔和定朔中國農曆的朔望月長度是平均29.5305天,所以農曆月就有大月30天,小月29天之分,從先秦時期到唐代,農曆曆法均是採用大小月輪流交替的方式設置每個農曆月的天數,只有少數情況下才出現連續兩個大月的情況,採用這種方式的曆法就稱為「平朔」。「平朔」曆法簡單,但是不能保證日月合朔發生在初一這一天,有可能是上月的月末一天,也有可能是本月初二。南北朝時期,一種新的曆法被提出來,這種曆法嚴格按照日月合朔為月初制定農曆月,採用這種方式的曆法就稱為「定朔」。「定朔」曆法嚴格將日月合朔時間確定月初,因為月球公轉是橢圓軌道,速度並不是均勻,所以會發生連續多個大月或連續多個小月的情況,導致「定朔」曆法推廣遇到很大的阻力,直到唐代,中國曆法才全面棄用「平朔」,改用「定朔」。小知識5:正月初一和立春節氣立春是二十四節氣之首,所以古代民間都是在「立春」這一天過節,相當於現代的春節(中國古代即是節氣也是節日的情況很多,比如清明、冬至等等)。1911年,孫中山領導的辛亥革命建立了中華民國,在從曆法上正式把農曆正月初一定為「春節」,把公曆1月1日定為「元旦」,也就是「新年」。農曆年從正月初一開始沒有爭議,但是農曆生肖年從何時開始卻一直有爭議,目前多數人都認為「立春」節氣是農曆生肖年的開始。因為在中國古代曆法中,十二生肖的計算與天干地支有很大關係,所以在「論天干地支、計算廿四節氣」的情況下,「立春」節氣應該是新生肖的開始。對於普通老百姓來說,習慣於認為正月初一是生肖年的開始,因此,正月初一和「立春」節氣之間出生的小孩,在確定屬相的時候就有點麻煩了。屬龍還是屬蛇?這是個問題。
推薦閱讀:

姓名與人生(教你如何計算天、人、地、總、外五格)
怎麼計算自己的生辰八字?
還款日期計算方法
536、 從上市公司取得的股息紅利所得,在計算個人所得稅時是否有優惠?
命卦計算公式

TAG:中國 | 演算法 | 計算 | 農曆 | 法系 | 算法 |