關於中國日曆部分演算法---摘自別人Blog - qsd007的專欄 - CSDN博客

本文是對最近做曆法研究時寫的 CnCalendar 單元的演算法總結與說明。

目前還是草稿,缺農曆演算法部分。

======================================================================

1. CnCalendar 介紹

======================================================================

    CnCalendar 旨在為國內外程序員尤其是 Delphi/C++Builder 程序員提供一開放源碼的曆法計算工具包,此包涵蓋了公曆農曆節氣干支生肖星座數九三伏出梅入梅日出日落等各個方面,雖然力求做到完整精確,但曆法本身的複雜性和天文現象的不確定性決定了這裡的計算不會太完善。為了給用戶一個完整的使用說明以及使用戶在發現問題時能通過閱讀 CnCalendar 的代碼解決問題,這裡將 CnCalendar 的曆法演算法說明整理如下文。

======================================================================

2. CnCalendar 演算法說明

======================================================================

    本節對 CnCalendar 所使用的演算法進行了詳細說明,歡迎閱讀並指出謬誤之處。

----------------------------------------------------------------------

2.1 概念與常識介紹

----------------------------------------------------------------------

    進行曆法計算前,有一些概念是需要首先明確的,包括曆法本身的概念、部分天文名詞、公曆年、月、日、時的定義等,CnCalendar 演算法中所涉及的概念都會在本節中加以說明。值得一提的是,部分概念是屬於天文現象、部分則是曆法推演而來,一般根據前者修正後者。前後兩者擬合的精確度,是評判曆法準確與否的依據。

2.1.1 陽曆、陰曆、陰陽曆

------------------------

    太陽和月亮是對人類生產活動影響最大的兩個天體,因此曆法基本上都是以日和(或)月的運行規矩為依據來計算的,純粹以太陽的運行規律來計算的曆法稱為陽曆,純粹以月亮來計算的則稱為陰曆,兩者兼而有之的自然就是陰陽曆了。公曆是典型的陽曆,回曆是陰曆(本文未涉及),中國的農曆則是陰陽曆。

2.1.2 歷理的依據

----------------

    曆法的作用有兩個,一是能精確計時(或者說是計日),二是要符合實際情況。這個實際情況包括很多內容,如季節更替、農耕活動、潮汐漲落等。實際情況造就了曆法,曆法反過來又應當能根據其規則去推演實際情況,這便是曆法的最終目的。目前通用的公曆是 1582 年制定的格里高利曆(Gregorian),它在前身儒略曆(Julian)的一年 365 日、四年一閏的基礎上增加了百年不閏、四百年又閏的規矩,形成了現在的通用公曆曆法。格里高利曆是以數值計算為歷理依據的,換句話說,按照格里高利曆的數值計算規則,可以往前後推任意多的天數而計算出當時的公元年月日來,至於是否符合實際情況,則只有到時候瞧著辦了。像格里高利曆的前身儒略曆就出現過硬算下去誤差積累導致春分日提前 10 天的事故,不過令人放心的是,太陽與地球的相對活動情況比較穩定,儒略曆只是擬合得不甚精確,而修改後的格里高利曆的精確度提高了許多,至少能支持三千年(如果加上四千年又不閏的規則則可以撐幾萬年了)。這也說明,只要曆法所根據的天文情況比較固定,曆法就可能推演成簡單數值計算的公式。公曆正是如此。

    中國的農曆計月所依據的是月亮的運行規則,月亮的不穩定度比太陽高得多,而農曆是天文曆法,其原則規定了以實際情況為準、數值計算為輔,所以導致了中國歷史上的多次修歷,更導致了目前農曆計算和公農曆轉換的複雜性。而且農曆這個陰陽曆還必須考慮到太陽周期(回歸年)和月亮周期(陰曆月)的配合,更產生了十九年七閏的規則,而閏月的確定還需要以節氣為準,大大增加了計算的複雜度。後文會詳細說明。

2.1.3 春分點,回歸年,陰曆年

----------------------------

    地球是斜著繞太陽轉的,所以赤道面和黃道面有個固定的夾角,這個夾角在地球公轉過程中導致了太陽直射點在南北回歸線間移動,因此造就了四季更替、形成了年的概念。注意年的概念比發現地球繞太陽轉要久遠得多,年對古人的直觀感受就是四季更替和春種秋收,這個感受和地球是否公轉是無關的。假想如果地球光自轉不公轉而太陽在垂直於黃道面的方向上來回移動,同樣也能造就年的感受。——說明這點的目的是為了區分下面所描述的概念:恆星年和回歸年。

    地球精確繞太陽轉一周的時間稱之為恆星年,而四季更替的精確時間稱之為回歸年。那麼怎樣衡量四季更替的精確時間?這就要用到春分點的概念。春分點是天文概念,它是黃道面上的地球公轉軌道和赤道面的兩交點之一(另一個自然是秋分點了),春分秋分點時太陽正好直射赤道,而太陽的直射又正好符合四季更替的感受要求,因此回歸年的定義就是地球從這個春分點轉一圈又回到春分點的時間。

    如果春分點不動,恆星年就等於回歸年,因為地球轉一圈又回來了。遺憾的是地軸總會像陀螺一樣有進動和章動,所以導致的歲差會讓春分點在一年中稍微前進一點點,因此一恆星年比一回歸年要長那麼一點點,一個是 365.25636 日,一個是 365.2422 日。記住,回歸年才是我們通常概念中的年。

    也許有人要問,恆星年和回歸年有 20 多分鐘的差距,長此累積下去會有什麼影響?答案是「和曆法無關」。曆法中本來就不管地球在公轉軌道上的位置,春分點秋分點的平分——冬至點和夏至點也和公轉軌道上的近日點遠日點沒什麼必然關係,所以關心恆星年和回歸年的差距是天文學家的事情。太陽曆的基礎就是回歸年。

    至於陰曆年,則沒有什麼天文意義,只是月的集合而已,不過在農曆這個陰陽曆中,它也得擬合回歸年的長度。一農曆年是從正月初一到次年正月初一前的時間,它可能包含 12 或 13 個月,取決於閏年的設定。

    農曆中還有「歲」的概念,是從冬至日到次年冬至日之間的時間,等於一回歸年。這個概念用來計算閏月。

2.1.4 陽曆月,農曆月

--------------------

    陽曆月同樣沒有什麼天文現象和它對應,設置陽曆月只是為了便於把年分割計時,所以儒略曆時能隨便將月份的日期調大調小,只要保持一年的天數不變就行。像七八月連大、二月特小等都是歷史上遺留下來的問題。

    農曆月則不同,農曆的月是陰曆月,反映了月相的變化,也就是月球繞地球公轉一周的周期。農曆規定了月朔時的那日為農曆月首初一,下個月朔時為下個月月首初一。由於月球的公轉周期差不多是 29.5 天但月必須是天的整數倍,因此農曆月有大有小,大月 30 天,小月 29 天,而且也間隔得無規律可循,也就是說沒法在不參考月相又脫離年份的情況下直接算出某個農曆月的大小來,這一點和公曆是不同的。

2.1.5 日,平太陽日

------------------

    日是地球自轉帶來的晝夜交替,時、分、秒都脫胎於它。以往日的定義是真太陽日,也就是在地面上觀測到的太陽視點繞地球一周又回到天球上天頂位置的時間間隔,這個間隔和地球精確自轉一周的時間(恆星日)有點差別,因為地球還在公轉,位置有點變化,其原理類似於春分日前進導致的回歸年和恆星年的差距。另外地球的黃赤夾角以及橢圓軌道上的公轉速度不均勻也導致了真太陽日的長短在一年中有所區別,為了消除這個差別,曆法中引入了平太陽的概念。「平」在曆法中是指時間均勻分布的意思,平太陽則是一個假想的太陽,以均勻的速度在天球赤道上繞地球運動,其速度的精確值為真太陽在一回歸年內的平均速度,並且和真太陽同時經過近地遠地點,這個平太陽運行一周回到天頂的時間則定義為一平太陽日,也就是現在通常的日的概念。換句話說,地球一年內的自轉轉速分布和公轉情況共同決定了一平太陽日的實際大小。

    至於時、分、秒,最初都是由平太陽日的定義衍生而來的:1 平太陽日等分為 24 小時,1 小時等分為 60 分鐘,1 分鐘等分為 60 秒。而後來由於地球自轉不均勻導致的平太陽日的精確度不高,便另起爐灶以其他更精確的手段規定秒的長度,則是以後的事情了。

2.1.6 節氣

----------

    節氣是對一回歸年內地球公轉軌道上的 24 個具有季節意義的等分位置的描述,這 24 個節氣名這裡就不贅述了,只是這個「等分」也有說法:中國古代採用平氣,按時間等分一回歸年,相鄰兩節氣之間時間間隔固定。清代修歷後改用定氣,等分周天為 24 分,每 15°為一個節氣,這樣相鄰節氣的時間間隔由於地球公轉速度的變化會有所不同。

    24 個節氣中包括 12 個節氣和 12 個中氣,奇數號為節氣,存在於每公曆月上旬,偶數號為中氣,存在於公曆月下旬。中氣對陰曆的閏月計算起著決定性的作用。

    本文的節氣計算和中國現代農曆一樣使用定氣。而古代使用的平氣,其交節時刻和根據定氣計算推得的時刻會有較大的誤差,計算時應予注意。

2.1.7 年月日時的天干地支

------------------------

    中國採用天干地支計法來計年月日時,十天干十二地支循環配成六十花甲再輪排,年月日時都有和其對應的干支。計算年月日時的干支俗稱「算八字」,當然本文的算八字是指根據年月日時推算出干支數,並非根據推算出來的四個干支再算命運財運桃花運等,那已經不屬於本文討論的範疇了。

----------------------------------------------------------------------

3.1 曆法演算法分類

----------------------------------------------------------------------

    在說明演算法之前,一些約定俗成的說法需要說明一下:

  * 公元前 n 年表示為 -n 年,公元 0 年不存在。

  * 不存在公元 1582 年 10 月 5 日到 14 日這 10 天

    (被 Gregory 刪掉了),但星期和干支仍然連續。

  * 計算而得的干支、生肖、星座、節氣的數字描述均以 0 為起始。

  * 求回歸年內第 N 個節氣交節時刻時,第 1 個代表小寒,

    這裡無第 0 個的說法。

    CnCalendar 目前的曆法演算法包括:

  * 星期

  * 公曆年月日時的干支

  * 節氣的公曆交節時刻

  * 星座

  * 日出日落時刻的計算

    下面對各個演算法分開進行簡要說明。具體實現可以參考 CnCalendar 中的代碼,裡面也有較為詳盡的注釋。

3.1.1 計算星期

--------------

    CnCalendar 提供了根據公曆年月日計算星期幾的方法,演算法來源於《新編萬年曆》一書的公元日數求余法(有細微修改),原理很簡單:星期的 7 天排列是和具體曆法無關的,只按照日數循環計算,因此只要知道某個公曆年月日距某個參照日的天數和此參照日的星期數,便可通過 7 求余法計算出公曆年月日的星期幾來。

    演算法中,距公元 1 年 1 月 0 日(公元前 1 年 12 月 31 日)的日數等於兩部分之和:基礎年日數和閏年修正日數。前者等於和公元 1 年的年份差乘以 365 再加上今年此日距今年 1 月 0 日的日數(包括此日本身,每個閏年裡多出的 1 日也計算在內)。後者等於年份差對 4 求余再減去個修正值。

    年份差是該年和公元 1 年的實際年份差距,當 N 為正時,公元 N 年的年份差為 N - 1,公元前 N 年的年份差為 N(因為無公元 0 年)。

    修正值是對儒略曆和格里高利曆切換期間的日期調整和對百年不閏四百年又閏規則的調整之和。其值規則如下:

    * 日期為 1582 年 10 月 5 日之前(含 5 日,以下同)時為 0。

    * 日期為 1582 年 10 月 15 日至 1700年 12 月 31 日時為 10。

    * 從 1701 年 1 月 1 日起增加 1,每增加一個世紀再加 1 ,

      但能被 400 整除的世紀不累加。如 1801 年元旦後修正值為 12,

      1901 年元旦後修正值為 13,2001 年元旦後修正值仍為 13。

      2101 年元旦後才為 14。

    算出實際日期後,減去 2,再對 7 求余,得到的即是星期值,0 對應星期日,1 對應星期一,由此類推。這兒減去 2 是因為公元 1 年 1 月 2 日才是星期天(0 日是星期五,因此 -2 也可用 +5 代替)。

3.1.2 計算干支

--------------

    計算年月日時干支的原理和計算星期差不多,因為其連續性,都可以用求余法來計算。不過中國用來計算干支的年月概念和公曆年月不同,年以立春為界,月以 12 節氣為界,計算前公曆的年月需要轉換成傳統中國曆法的年月。

3.1.2.1 年干支

    計算年干支需要公曆年月日三參數,無月日參數則默認認為是立春之後的某日,這樣不需要調整年份。如有月日參數,則判斷其是否處於本年立春交節時刻之前,之前則屬於去年,年份減一。得到轉換的年份後,假設為 Year,如其大於 0,則干支數為 (AYear - 1 - 3) mod 60,其中 Year - 1 為到公元 1 年的年份差,Year 為負數則根據無公元 0 年調整為 (AYear + 1 - 1 - 3) mod 60,這個 -3 源於公元 1 年是辛酉年,而辛酉在六十花甲的 0 ~ 59 中排行 57。

3.1.2.2 月干支

    公曆月份的干支數雖可以用求余法來計算,不過因為一年的月份數等於地支數,和天干數的關係也比較簡單,因此有更簡便的口訣演算法(口訣來源於《新編萬年曆》一書,網上也有類似文章)。首先仍然需要根據當年的立春和各個節氣交節時刻來調整年月數,然後計算出本年干數,年干數和本年首月月干數有口訣對應,這裡把冗長的口訣簡化了一下寫在注釋里,用代碼描述如下:

  case Gan of // 根據口訣從本年干數計算本年首月的干數

    0,5: // 甲己 丙佐首,

      Result := 2;

    1,6: // 乙庚 戊為頭,

      Result := 4;

    2,7: // 丙辛 尋庚起,

      Result := 6;

    3,8: // 丁壬 壬位流,

      Result := 8;

    4,9: // 戊癸 甲好求

      Result := 0;

  end;

    現行農曆規定正月為寅。得到首月的干支數後,便可序次推得該月干支數。

3.1.2.2 日干支

    日干支和星期的計算有類似之處,按星期計算的法子算得公元日數後,加 12 再對 60 取余則得到當日的干支數,12 源於公元 1 月 0 日是丙子日,而丙子在六十花甲的 0 ~ 59 中排行 12。

3.1.2.3 時干支

    一天 24 小時對應著 12 個時辰,時辰和干支對應,但時辰的分界卻不嚴格對應著日的分界。前一日 23 時到本日 1 時為子時,1 到 3 時為丑時,依此類推。

    時干支和月干支的計算類似,首先根據小時數轉成時辰數,如果是 23 時以後,則日期調整為後一天。接著計算出本日干數,本日干數和本日子時干數也有簡單的對應關係,不過這次沒找到口訣,大概因為對應比較簡單。此對應關係用代碼描述如下:

  case Gan of

    0,5:

      Result := 0;

    1,6:

      Result := 2;

    2,7:

      Result := 4;

    3,8:

      Result := 6;

    4,9:

      Result := 8;

  end;

    可以簡化成:

  if Gan > 4 then

    Dec(Gan, 4);

  Gan := 2 * Gan;

    所以不需要口訣了。

    得到子時干數後,便可序次推得本日內時辰的干支數。

3.1.3 計算節氣

--------------

    節氣的定義雖然簡單,可要計算出比較精確的交節時刻卻並不容易,原因就是春分點在一回歸年內會有細微移動,所以太陽兩次通過各個定氣點的相隔時間並不是一精確回歸年,便沒法以 365.2422 日為周期來直接計算各個節氣的時刻。CnCalendar 的演算法移植自中國日曆類中作者根據曾次亮著作而擬合的節氣演算法,考慮到了地球自轉的進動與章動,因此平氣定氣的精確度據作者說能達到 10 分鐘左右。

3.1.4 計算星座

--------------

    星座計算比較簡單,僅僅根據月日就可以確定黃道十二宮的對應數值,規則如下:

    01.20 - 02.18:水瓶座

    02.19 - 03.20:雙魚座

    03.21 - 04.20:白羊座

    04.21 - 05.20:金牛座

    05.21 - 06.21:雙子座

    06.22 - 07.22:巨蟹座

    07.23 - 08.22:獅子座

    08.23 - 09.22:處女座

    09.23 - 10.22:天秤座

    10.23 - 11.22:天蠍座

    11.23 - 12.21:射手座

    12.22 - 01.19:摩羯座

    但黃道十二宮以白羊居首,所以代碼中返回的星座序號中 0 對應著白羊,水瓶雙魚分別是 10、11。

    另外星座名稱有不同說法,如摩羯又稱山羊、水瓶又稱寶瓶,射手又稱人馬、天秤又稱天平等,這裡只採用比較常用的說法。

3.1.5 計算農曆

--------------

3.1.5.1 農曆曆法詳細規則

    中國的農曆在 1929 年以前以北京當地的經度 116°25′為計算基準,之後以北京時間也就是東經 120°的東八區標準時間為計算基礎。農曆有四條基本規則:

    1. 月朔時刻所在之日定為農曆月初一。

    2. 如某節氣交節時刻落於初一本日內(0 時後到 24 時前),

       即使其交節時刻早於月朔時刻,也算落在此月中。

    3. 冬至必須出現於農曆十一月內。

    4. 相鄰的冬至交接時刻間(歲內)如出現 13 次月朔也就是 12 個完整

       的農曆月,則此歲稱為閏歲,此歲中有一個月需要設置成閏月。

       因為期間必然有 12 個中氣,所以至少有一個農曆月沒有中氣落入其

       中,閏歲內第一個沒有中氣的月份定為閏月,月份數與前一月相同。

    對於第四條規則,需要強調的是年內和歲內兩個的概念是不同的,如出現歲內有兩個農曆月無中氣的情況,則以第一個為閏月,而此閏月可能不會落在本年內。而如果歲內有一農曆月無中氣但本歲只有 12 次月朔也就是 11 個完整的農曆月的情況下,那個無中氣的月並不會被置閏。典型的例子是 2033 年閏 11 月:2033 農曆年第 8 個農曆月和第 11 個農曆月都無中氣,但因為 2032 農曆年冬至(11 月)到 2033 農曆年冬至(11 月)間只有 11 個完整的農曆月,所以不是閏歲,無需加入閏月。2033 年冬至(11 月)到 2034 年冬至(11 月)間有 12 個完整的農曆月,所以是閏歲,以冬至日後出現的第一個無中氣的農曆月為閏月,正好是 11 月後面的一個農曆月,於是就閏 11 月。

    總的說來一句話,無中氣只是閏月的必要條件並非充分條件,只有歲內月朔太多需要置閏月時,才會尋找歲內第一個無中氣的作閏月。舊版曆法以 2033 年第 8 農曆月為閏 7 月,只因為它是年內第一個無中氣的農曆月,並未考慮到是否閏歲的情況。新版已經改為了閏 11 月。

    另外,由於目前節氣採用定氣,地球在遠日點時運動速度慢,導致中氣在冬天裡推遲比夏日裡慢,因此無中氣的月份大多都落在夏天前後,幾百年來極少有出現閏 12 月的,這基本保證了春節的唯一。農曆曆法的精妙可見一斑。

    註:中國現代的曆法以南京紫金山天文台發布的內容為權威。如本文的闡述和該權威有衝突,以該權威為準。

3.1.5.2 農曆計算

    從上文的農曆規則來看,農曆完全是天文曆法,加上月亮運動規律的不穩定,導致很難對其進行精確計算。目前權威的計算方法是根據天文觀測數據建立月球、地球、太陽的運行數學模型來精確計算各個朔日與中氣的時刻再加以曆法推算。

    (待補充)

3.1.6 計算數九與三伏

--------------------

    數九也稱九九,是從冬至日開始的 81 天,每 9 天稱為一個九,第 10 天稱為二九第一天,冬至日本身是一九第一天。CnCalendar 中計算某公曆年月日的九九數時首先算出本年的冬至日,如果此日在冬至日開始後的 81 天內,則表明在九九日內,可通過整除求余等求出是幾九的第幾天。如果此日落在本日冬至日前,則檢查是否處於上一年的冬至日的 81 天后,是則再計算一遍,因為九九日一般都會跨越兩個公曆年的交界時。

    伏日的計算稍微複雜點,按傳統,每年夏至後的第三個庚日(天干)起的 10 天為初伏,夏至後的第四個庚日起為中伏,如果第五個庚日落在立秋前則中伏算 20 天,否則 10 天,立秋後的第一個庚日起的 10 天為末伏。另外計算庚日時夏至立秋本身不計算在內,也就是說如果夏至日本日是庚日,它的後 10 天才算第一個庚日,立秋與此相同。

3.1.7 計算入梅出梅

    入梅和出梅是指江南一帶梅雨季節的開始和結束,入梅日規定為芒種之後的第一個丙日(天干),出梅日則是指小暑之後的第一個未日(地支),和上面的類似,計算日的天干地支數時芒種和小暑本日同樣不計算在內。

3.1.8 計算日出日落

------------------

    該部分演算法移植自「時間科普網站」,此處無法詳細闡明。

======================================================================

3. 參考內容與致謝

======================================================================

    CnCalendar 的開發主要參考了以下內容,在這裡對作者們的無私奉獻表示敬意。

  * 《新編萬年曆》,實際有百年左右,包括一些曆法說明與計算。

  * 「知來者」日曆源碼以及「中國日曆類」源碼,JavaScript 編寫,

     節氣演算法自此移植而來。

  *  林洵賢的萬年曆 JavaScript 源碼,參考。

  * 「日梭萬年曆」5.2 beta 版,用來核對部分計算結果。

  * 「時間科普網站」:http://www.time.ac.cn,

     日出日落計算代碼自此移植而來。

 

本來以前以為陽曆轉陰曆的演算法可能並不難,今天有個網友給了段代碼給我一看,我就暈了,才知道陰曆演算法這麼的複雜,然後我就將這個網友給的代碼收藏了一下。以備後用 

 

真恐怖,沒有點深厚的數學知識還真沒有辦法寫出完整的演算法,中國的文化果然博大精深,牛X牛X 

 

http://www.ainlife.cn/uploads/200605/10_233851_.rar
推薦閱讀:

男人著裝的20條「鐵律」 - EaSy『s Blog

TAG:中國 | 演算法 | 博客 | 日曆 | CSDN | 算法 | 關於 | Blog | 專欄 |