C++算法系列之日历生成的算法
所谓的“天文算法”,就是利用经典力学定律推导行星运转轨道,对任意时刻的行星位置进行精确计算,从而获得某种天文现象发生时的时间,比如日月合朔这一天文现象就是太阳和月亮的地心黄经(视黄经)差为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年(公历年)有关的朔望月关系表:
表(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个节气时间,时间可以跨年份,内部判断过冬至节气后自动转到下一年的节气继续计算:
void CChineseCalendar::GetAllSolarTermsJD(int year, int start, double *SolarTerms) { int i = 0; int st = start; while(i < 25) { double jd = CalculateSolarTerms(year, st * 15); if(st == WINTER_SOLSTICE) { year++; } st = (st + 1) % SOLAR_TERMS_COUNT; } }
start参数是节气的索引,定义二十四节气的索引如下:
const int VERNAL_EQUINOX = 0; // 春分 const int CLEAR_AND_BRIGHT = 1; // 清明 const int GRAIN_RAIN = 2; // 谷雨 const int SUMMER_BEGINS = 3; // 立夏 const int GRAIN_BUDS = 4; // 小满 const int GRAIN_IN_EAR = 5; // 芒种 const int SUMMER_SOLSTICE = 6; // 夏至 const int SLIGHT_HEAT = 7; // 小暑 const int GREAT_HEAT = 8; // 大暑 const int AUTUMN_BEGINS = 9; // 立秋 const int STOPPING_THE_HEAT = 10; // 处暑 const int WHITE_DEWS = 11; // 白露 const int AUTUMN_EQUINOX = 12; // 秋分 const int COLD_DEWS = 13; // 寒露 const int HOAR_FROST_FALLS = 14; // 霜降 const int WINTER_BEGINS = 15; // 立冬 const int LIGHT_SNOW = 16; // 小雪 const int HEAVY_SNOW = 17; // 大雪 const int WINTER_SOLSTICE = 18; // 冬至 const int SLIGHT_COLD = 19; // 小寒 const int GREAT_COLD = 20; // 大寒 const int SPRING_BEGINS = 21; // 立春 const int THE_RAINS = 22; // 雨水 const int INSECTS_AWAKEN = 23; // 惊蛰
节气索引乘以15就是节气在黄道上对应的度数。GetNewMoonJDs()函数从指定时间开始连续计算15个朔日时间,从第一个冬至节气前的第一个朔日开始。15个朔日可以形成14个完整的朔望月,保证在有闰月的情况下也能包含两个冬至节气:
void CChineseCalendar::GetNewMoonJDs(double jd, double *NewMoon) { for(int i = 0; i < NEW_MOON_CALC_COUNT; i++) { double shuoJD = CalculateMoonShuoJD(jd); NewMoon[i] = shuoJD; jd += 29.5; /*转到下一个最接近朔日的时间*/ } }
BuildAllChnMonthInfo()函数根据15个朔日时间组成14个朔望月,根据相邻朔日的间隔计算出农历月天数用来判定大小月,并且从“十一月”开始依次为每个朔望月命名(月建名称):
bool CChineseCalendar::BuildAllChnMonthInfo() { CHN_MONTH_INFO info; //一年最多可13个农历月 int i; int yuejian = 11; //采用夏历建寅,冬至所在月份为农历11月 for(i = 0; i < (NEW_MOON_CALC_COUNT - 1); i++) { info.mmonth = i; info.mname = (yuejian <= 12) ? yuejian : yuejian - 12; info.shuoJD = m_NewMoonJD[i]; info.nextJD = m_NewMoonJD[i + 1]; info.mdays = int(info.nextJD + 0.5) - int(info.shuoJD + 0.5); info.leap = 0; CChnMonthInfo cm(&info); m_ChnMonthInfo.push_back(cm); yuejian++; } return (m_ChnMonthInfo.size() == (NEW_MOON_CALC_COUNT - 1)); }
CalcLeapChnMonth()函数根据节气和朔日时间判断在两个冬至节气之间的农历年是否有闰月,判断的依据就是看第十四个朔日是否在第二个冬至节气之前,如果第十四个朔日发生在第二个冬至节气之前,就说明在两个冬至节气之间发生了十三次朔日,需要置闰月。因为农历中十二个中气属于哪个农历月是固定的,因此置闰月的过程就是依次判断十二个中气是否在对应的农历月中,如果本应该属于某个农历月的中气却没有落在这个农历月中,则这个农历月就是闰月,需要设置闰月标志,同时调整这个月之后的月名。调整农历月名的方法就是月名减一,比如原来是八月就要调整为七月,这样就将十三个月对应上了十二个月名(其中多出来的一个农历月被命名为闰某月)。如果节气和朔日发生在同一天,CalcLeapChnMonth()函数采用的是民间历法的规则,与现行历法一致:
void CChineseCalendar::CalcLeapChnMonth() { assert(m_ChnMonthInfo.size() > 0); /*阴历月的初始化必须在这个之前*/ int i; if(int(m_NewMoonJD[13] + 0.5) <= int(m_SolarTermsJD[24] + 0.5)) //第13月的月末没有超过冬至,说明今年需要闰一个月 { //找到第一个没有中气的月 i = 1; while(i < (NEW_MOON_CALC_COUNT - 1)) { /*m_NewMoonJD[i + 1]是第i农历月的下一个月的月首,本该属于第i月的中气如果比下一个月 的月首还晚,或者与下个月的月首是同一天(民间历法),则说明第i月没有中气*/ if(int(m_NewMoonJD[i + 1] + 0.5) <= int(m_SolarTermsJD[2 * i] + 0.5)) break; i++; } if(i < (NEW_MOON_CALC_COUNT - 1)) /*找到闰月,对后面的农历月调整月名*/ { m_ChnMonthInfo[i].SetLeapMonth(true); while(i < (NEW_MOON_CALC_COUNT - 1)) { m_ChnMonthInfo[i++].ReIndexMonthName(); } } } }
从理论上讲,本文介绍的算法在精度允许的范围内可以计算前后几千年的农历年历,但是对古代的农历计算需要小心。首先是“平朔”和“定朔”的问题,唐代以前使用的是平朔方法定月首,本文介绍的计算方法采用的是“定朔”方法,因此计算出的年历与唐代以前的历史会不一致。另外,即是在唐代以后采用“定朔”的历法,因为古代天文观测和计算受条件限制,可能不够精确,因此与现在用天文算法计算出的结果可能并不一致。所以对历史农历的计算应该以历史事实为主,天文计算为辅,当计算与历史不一致时,要根据历史数据进行校正。Calendar.exe是根据本文介绍的算法编写的日历小程序,没有太多的功能,主要是为了验证算法,因为没有历史数据用于修正结果,因此不支持1601年以前的农历计算(也就是说按照天文算法计算出来的结果可能和实际历史上的历法不符)。
图(5)演示程序的界面
小知识1:民间历法和历理历法
新中国成立以后没有颁布新的“官方农历历法”,将历法和政治分离体现了时代的进步,但是由于没有 “官方历法”,也引起了一些问题。比如我国现在采用的农历历法是《时宪历》,它源于清朝顺治年间(公元1645)颁布的《顺治历》,它有两个不足之处:一个是日月合朔和节气的时间以北京当地时间为准,也就是东经116度25分的当地时间,其节气和新月的观察只适用于中原地区。其它经度的地方,因为时间的关系,对导致日月合朔和节气时间的差异导致置闰和月顺序各不相同。另一个不足之处就是日月合朔时间和节气时间判断不精确,如果日月合朔时间和节气时间在同一天,不管具体的时间是否有先后,一律将此节气算做新月中的节气,这样一来,如果这个节气是中气,就会影响到闰月的设置。历理历法针对这两点进行了改进,对节气时间和日月合朔时间统一采用东经120度即东八区标准时,这样在任何时区的节气和置闰结果都是一样的,以东八区标准时为准。对于节气时间和日月合朔时间在同一天的情况,精确计算到时、分、秒,只有日月合朔时间在节气时间之前,这个节气才包含在次月内。历理历法从理论上讲更符合现代天文学的精确计算,但是需要注意的是,历理历法仍然只是存在于理论上的历法,我国现行的农历历法依然是民间历法《时宪历》或《顺治历》。
小知识2:通式寿星公式
“通式寿星公式”是前人整理出来的一个用于计算每年立春日期的经验公式:
Date = 向下取整(Y * D + C) - L
其中,Y是年份,D的值是0.2422,C是经验值,取决于节气和年份,对于21世纪,立春节气的C值是4.475,春分节气的C值是20.646等等;
L是闰年数,其计算公式为:
L = 向下取整(Y/4) - 向下取整(Y/100) + 向下取整(Y/400)
用“通式寿星公式”确定2011年立春日期的过程如下:
L = int(2011/4) – int(2011/100) + int(2011/400) = 502 – 20 + 5 = 487
Date = 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日定为“元旦”,也就是“新年”。农历年从正月初一开始没有争议,但是农历生肖年从何时开始却一直有争议,目前多数人都认为“立春”节气是农历生肖年的开始。因为在中国古代历法中,十二生肖的计算与天干地支有很大关系,所以在“论天干地支、计算廿四节气”的情况下,“立春”节气应该是新生肖的开始。对于普通老百姓来说,习惯于认为正月初一是生肖年的开始,因此,正月初一和“立春”节气之间出生的小孩,在确定属相的时候就有点麻烦了。属龙还是属蛇?这是个问题。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持自由互联。