现在的位置: 首页 > 综合 > 正文

日历相关的东西和算法

2013年12月01日 ⁄ 综合 ⁄ 共 10099字 ⁄ 字号 评论关闭

最近做的东西需要处理一些日历相关的东西,包括农历,仔细调用后发现看似简单的日历后面有许多复杂和有趣的问题。因此搜索了一些资料,整理出来。

日历相关的地理知识

    
人类最早关注历法是由于生活的需要,比如每天的日升日落和月升月落帮助调节生物钟,周期性的规律让人发现时间的存在。四季的更替,春华秋实,每个季节都有
不同的食物,农业的发展让我们的祖先更加重视太阳的运动规律。日食月食等天文现象让人恐惧,星宿的变化似乎预示这某位重要人物的降临或离开。。。所有这些
都使得历法在古人的生活中变得非常重要。  
这些天文现象影响了人类生活的方方面面,古人也对此做出了自己的解释,现代科学的萌芽也与天文密切相关。在中国,统治者自命“天子”,顺应“天命”,如果
不是风调雨顺,那么必然是统治者触犯上天,所以解释和预测天文现象是天子的职责。而在西方,教皇是政治和宗教的领袖,其权力的合法性也与上天密切相关。所
以近代科学与宗教的斗争集中体现在天文学上,比如我们熟悉的哥白尼和伽利略。其实在中国的历史上也过类似的斗争,比如西洋传教士和中国保守的天文学家的斗
争,最出名的要数汤若望。除了对于历法的优劣比拼之外,更反映了朝廷的政治斗争。结果是汤若望被鳌拜逮捕,后来被康熙平反。

     为了理解日历,下面让我们回顾一下与历法相关的高中的地理知识吧。

    地球自转

         自西向东,轴心指向北极星。自转一周24小时(太阳日),恒星日为23时56分4秒。【时间最初的定义就是用太阳日的,所以是整数】  

         引起昼夜更替。【如果没有自转,那么一昼夜就是一年了】

    地球公转

         自西向东,轨道为椭圆,太阳位于一个焦点上,一个回归年为365.24219879日,一个恒星年为365.2564日【回归年定义为太阳从一个春分点到下一个春分点的时间】

         为什么恒星年会比回归年稍长一些,那是由于月球、太阳和行星的引力影响,使赤道部分比较突出的椭形地球的自转轴绕黄道作缓慢的移动(相应的春分点自黄道以每年20.24分速度西   

         退, 差不多71年西移1度,大约25800年移动一周),即岁差现象。

    黄赤交角  

          由于黄赤交角的存在,导致太阳直射点在南北回归线内交替变化,而不是始终直射赤道,从而引起四季的更替。    

          公历(Gregorian Calendar)的四季变化点: 春分秋分(Equinox),冬至夏至(Solstice)。二分点昼夜时长相等,二至点昼夜分别达到最长。

          黄道 黄道吉日

           地球绕太阳的轨道叫黄道,不过在地心说的古代,黄道就是太阳绕地球的轨道。黄道和赤道相交的日子,也就是春分或者秋分,被叫做黄道吉日,民间传说这天适合婚嫁,不过一年才两天似乎太少,所以老黄历有一套计算黄道吉日的方法。

     二十四节气

          除了4个季节变化点,中国古代把360°的地球轨迹又分成24节气,每个节气运动15°,这对指导农业生产至关重要。另外中气对于农历的历法计算也非常重要。

          二十四节气中,偶数的节气叫“中气”。 比如“雨水”,”春分“。

          我们小时候背过的二十四节气歌:春雨惊春清谷天,夏满芒夏暑相连,秋处露秋寒霜降, 冬雪雪冬小大寒。    

     时区与夏令时

         
由于地球自转一周24小时候,每个经度上的人看到太阳的时间都不相同,为了方便,每隔15°经度,设置一个时区。比如北京是在东八区,所以中国都使用北京
时间,但是中国横跨的经度很大,比如西藏,所以他们的作息时间可能比我们晚。他们可能14点才吃午饭。

          冬天和夏天的昼夜时长存在差异,为了充分利用阳光,很多国家实行了夏令时,也就是夏天到了后,时钟拨快一个小时候,夏天完了再拨回来。中国也实行过几年夏令时,不过后来取消了。

     协调世界时(coordinated universal time;UTC)  世界时 格林尼治标准时(GMT)

          
有了太阳或者月亮的周期性规律,那么就可以定义时间了。最简单的就是利用太阳------日出而作日落而息。但是碰到阴天或者雨天没太阳就惨了。不过生物
都有生物钟,个体的生存问题倒也不是特别严重。但是对于群居性的动物,人类的交流需要准确的时间,所以有必要准确的定义时间。那么把太阳的起落作为一个时
间单位是最自然不过的了,这样就有了”天“这个单位,但是天的粒度太大,所以古代又定义了时辰,相对于现在的两小时。由于地球绕太阳的运动并不规律,所以
真太阳日每天都不同,平太阳日虽然准确一些,但是也是变化的。现在最准确的计时方法应该是原子时钟。原子时秒的定义是:铯 -133
原子基态的两个超精细能级间在零磁场下跃迁辐射9192631770周所持续的时间。世界时的定义是以本初子午线的平子夜起算的平太阳时。又称为格林尼治
时间。但是这两种计时的精度不一样,原子时的精度为纳秒,而世界时为毫秒,协调世界时(UTC)是一种折中方案,并且在互联网上使用,比如网络时间协议
(NTP)就是使用的UTC时间。

           
如果大家仔细观察UTC的缩写,可能会觉得奇怪,协调世界时的三个英文单词的缩写应该是CUT,怎么是UTC呢?这里还有个小故事。它的法文是Temps
Universel Cordonné,缩写是TUC,当时制定游戏规则的人都想用自己母语的缩写,争来争去达成妥协,谁的都不用,就用UTC了!!!

 

历法的相关概念

     公历(Gregorian Calendar)

            又叫格列历,阳历,太阳历。中国在辛亥革命后的民国元年采用。

            它的前身是儒略历(Julian
calendar),这是西方十六世纪前采用的历法,有儒略·凯撒颁发。它一年12个月,月的天数就是我们现在公历的方法,除了2月守闰年的影响,其余各
月天数固定,并分大小月。4年一闰年。这样平均一年365.25天。比实际公转周期的365.2422日长11分14秒,即每400年约长3日,所以为了
矫正这个误差,公历的闰年又加了一条规则:如果能被100整除,那么必须被400整除。以前觉得这个规则很诡异,看到这个后就非常自然了------相对
于每400年去掉3个闰年。这样每年平均长365.2425日,与公转周期的365.2422日十分接近。可基本保证到公元5000年前误差不超过1天。

      阴历(lunar calendar)

           
按月亮的月相周期来安排的历法。由于月球绕地球的转动周期是29.5天左右,所以如果一年设12个月,那么一年大概为354或者355天。按照月亮的月相
来计时的方法的优点是简单,缺点是这样一年的时间和太阳年相差了11天左右。中国的农历在民间也被称为阴历,其实这种叫法是不准确的,中国的农历结合了阳
历和阴历,并且以阳历为主,阴历只是一个时间单位。真正的阴历只有伊斯兰的历法,它的主要用途是指导他们的宗教节日等,因此穆斯林的斋戒节有时在夏天,有
时在冬天。但是阴历没法指导生活,所以在伊斯兰国家另外设置阳历来指导生活和农业生产。

      农历

             中国以前使用的历法,又称阴历,夏历。   

            
前面说了,阴历每年差了11天左右,19年就是209天,除以阴历每月29.5天就是7.084。这种方法由古希腊人默冬在公元前432年提出:在19个
阴历年中安置7个闰月,即可与19个回归年相协调。其实在他之前100多年中国人就已经发现了这个规律。这个周期的英文名字叫Metonic
cycle,中文一般翻译成默冬章而不是默冬周期。因为这个周期在中国历法叫一个”章“。

             这就是19年里设置7个闰月的缘故。那这7个闰月放到19年中的哪些年中呢?如果是闰年,这个闰月放到哪个月后呢?这是中国古代历法需要解决的重要问题。

             3年一闰,5年二闰,19年七闰,这是安放闰年的方法,其依据没找到,或许它的好处是使得每年的时间尽量接近太阳年吧。

             那么如果这年是闰年,那么闰月放到哪呢?

            
农历闰月的安插,自古以来完全是人为的规定,历代对闰月的安插也不尽相同。秦代以前,曾把闰月放在一年的末尾,叫做“十三月”。汉初把闰月放在九月之后,
叫做“后九月”。到了汉武帝太初元年,又把闰月分插在一年中的各月。以后又规定“不包含中气的月份作为前一个月的闰月”,直到现在仍沿用这个规定。

              另外阴历一个月29.5天,所以又有大月和小月的说法。大月30天,小月29天。但哪个月大月,哪个月小月,算法就比较复杂了,不像公历那么简单。

              这样一来,农历一年可能的天数包括354,355,356,383,384,385。

          农历的年和岁

              前面说过了,中国的农历其实是以阳历为主的,毕竟农历最重要的用途就是指导农业生产。所以农历的定义为一个冬至日到下一个冬至日的时间。【这和公历年的定义很相似,不过公历定义的是春分日到下一个春风日,如果地球运动是规律的椭圆的话,那么这两个时间应该完全一样,不过事实上它们还是有0.004天的差别】

              过去中国人说年龄,一般说多少岁,就是这个岁。有的地方认为过了冬至就长了一岁,有的地方是过春节或者立春。

              前面说了中国的农历有24节气,其中对于历法来说,冬至是最为重要的节气。周朝的时候以冬至所在的月为一年的开始。汉代以后,这个月变成十一月,它之后的第二个月成为正月。正月初一为春节。

              由于1岁=12.37月,如果这一个岁包含了完整的12个月【朔望月,也就是大约29.5日】,那么这一个岁就叫闰岁。由于一岁包含至少13个月,而只有12个中气,那么至少有一个月没有中气,因此农历规定没有中气的月为闰月,闰月的天数和上一个月一样多,包含闰月的年叫闰年
              奇怪的2033年

               这一年的11月是闰月,所以这年是闰年,但它不包含12个完整的月,所以不是闰岁。2034年是闰岁,但不是闰年。

              聋子年 双春年

                如果立春在春节前,那么这年就叫聋[子]年,比如2010年就是,如果这个聋子年下半年还不包含另一个立春,那么就叫双聋年

                如果这年包含两个立春,那么就叫双春年。

            一个月有多少天        

              
和公历相比,农历是一种”测量“的历法,而不是”算术“的历法。也就是说,公历的月的大小都是人为指定的,可以预先计算的;而农历的年月需要”测量“,比
如测量冬至日来确定岁,同样,月也是测量月亮的运动来确定的。所以农历的”计算“会很复杂,而且如果地球或者月球的轨道发生变化【比如科幻小说里的火星撞
地球之类】,那么它也会跟着发生变化。当然,科学家预测最近的几百年应该不会有太大的变化,所以制作”百年历“还是没有问题的,”万年历“就不好说啦。

               测量点(子午线)是在东经120°

              
由于需要的测量月亮的变化来确定,所以测量点非常重要。1929年前的测量点都是在北京,经度为东经116°25′,使用的时区确是120°的东八
区,1949年后搬到了南京紫金山天文台,这个地方的经度是东经118°46′。这个变化看似微小,却引发了1978年”连续两天中秋节“的故事。

               农历每天的开始是凌晨0:00

               有的历法是把中午12点作为一天的开始

               农历里新月是一个月的第一天

               有的历法把满月后的一天作为下月第一天

               因此,月的”计算“也会比较复杂,而且会出现连续多个”大月“的情况,比如1990年十月到1991年一月连续4个大月。

               十五的月亮十六圆?

               农历的月采用月相的变化来表示,那么十五应该是满月的时候。八月十五中秋节作为传统的节日,所以大家特别关注这晚的月亮。但是民间有”十五的月亮十六圆”的说法,这样什么道理吗?

                经过统计[1],从1984年到2049年,满月出现在十五的次数为306,而出现在十六的次数为380,另外十七124次,十四6次。所以从统计意义上说十六确实更圆一些。

              干支纪年法              

                我们现在说2011年五月五日(端午节)其实是非常不伦不类的,农历的说法应该是辛卯年五月初五。公元纪年是公历的用法,我国过去是干支纪年法。

                十天干:甲(jiǎ)、乙(yǐ)、丙(bǐng)、丁(dīng)、戊(wù)、己(jǐ)、庚(gēng)、辛(xīn)、壬(rén)、癸(guǐ)。  

                十二地支:子(zǐ)、丑(chǒu)、寅(yín)、卯(mǎo)、辰(chén)、巳(sì)、午(wǔ)、未(wèi)、申(shēn)、酉(yǒu)、戌(xū)、亥(hài)。

                 如果两两组合,那么共有120中可能,不过天干地支用来纪年是并不是任意两个组合都可以的。

                它的方法是先天干和地支两两配对. 甲子 乙丑  丙寅  丁卯 .戊辰 己巳  庚午 .辛未  壬申  癸酉,这时天干用完了,地支还剩两个,于是天干循环使用甲戌 乙亥

                这时地支也用完了,那么也循环使用,最终得到60中可能的组合,仔细分析,其实就是天干和地支奇数和奇数能配对,偶数和偶数能配对。所以如果有人说甲丑年,那么别忙着算啦。

                这样的方法只能计数一甲子,也就是60年,然后循环使用,所以60年前和60年后的干支纪年没有差别。

              年号

                
干支纪年似乎有“歧义”,不过过去的皇帝都会在自己的统治期间设置年号,这样就不会有歧义了。在明清之前,很多皇帝经常更换年号,而到了明清,每个皇帝就
只有一个年号了。所以我们称康熙皇帝而不会称贞观皇帝。一般皇帝执政时间不会超过60年,所以年号+干支能唯一标识一个年,但是像康熙皇帝执政了61年,
这就有点麻烦了。

                 民国是年号?

                 辛亥革命后到1949年前人们废除了干支纪年的方法,并且使用了公历,但是民间农历依然盛行,却没法说宣统xx年了,所以使用了民国xx年。这种纪年法1949年在大陆被废除,采用了公元纪年法,但是现在在台湾,人们仍然说民国xx年。

              生肖,八字

                  不知什么时候,人们把十二地支和12种动物联系起来了,这样每个年都被叫做鼠年或者虎年。注意,生肖也是跟农历相关的,准确的说以立春为起点。所以比如我,虽然出生在83年(猪年),确实属于狗的。

                  另外过去除了纪年用干支,纪月纪日和纪时也用干支,这样一个人的出生的年月日时就能用八个字来表示了,这就是八字。

              农历的现实意义

                  
从上面我们已经知道,农历里用来指导农业生产最重要的其实是二十四节气(其实也就是公历),而使用了月来作为度量时间的单位,但是太阳回归年和朔望月之间
没法匹配,所以引入闰年。农历的阴历部分的现实意义和伊斯兰历很像了,左右基本只剩下指导节日了,比如现在中国最重要的节日春节,中秋节,元宵节,端午节
等。另外中国很多节日是公历(节气)相关的,比如清明节,冬至节,芒种节等等。

 

农历和公历的换算

             上面说了一大堆,可能工程师最关心的就是换算了。因为日常生活使用公历就足够了。但是有些传统节日的计算需要把农历转换成公历。

              也就是说把农历转成公历是最常见的用途,不过如果做个什么算命网站,公历转农历也是需要的。

             前面说过了,农历是一种“测量”算法,所以准确的计算需要“预测”太阳和月亮的运行轨迹,这是天文局的任务。对于软件工程师来说,一般都是把官方计算的结果保存下来。

            
一般只需要保存这年是否闰年,如果是,闰的是几月。另外还需要知道这年每个月的大小。网上搜索一般能找到1800-2200年的,由于来源不明,是否正确
我也不敢保证,用最近的一些节日验证倒没有大问题。比如[http://netfork.iteye.com/blog/277221
这篇博客就是这样的算法,使用一个数组lunarInfo保存了1900年-2050年每年的信息。比如2009年0x0cab5,最后一个5表示闰五
月,如果最后一个为0,那么就是没有闰月,第一个字节0表示闰月是小月,中间3个字节12bit表示一到十二月是否大月。

             另外一个比较“靠谱”的软件是ICU4J了,这是IBM为了处理国际化问题做的一个包,感觉应该比较靠谱一点。

             ICU4J可以在icu-project.org/ 下载(需要翻墙)

            
它提供了一个ChineseCalendar的类,接口非常类似java.util.Calendar(ICU4J有自己的Calendar接口和
GregorianCalendar)。它可以传入一个Date对象,然后可以使用Calendar.get()方法返回对应的年月日。不过它返回的年是
干支纪年的年。当然要转换成公历的年也很容易。

             这样我们就可以把公历转农历:

      SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
      Date date=df.parse("2011-6-6");
      ChineseCalendar cal=new ChineseCalendar(date);
      System.out.println(cal.get(ChineseCalendar.YEAR));
      System.out.println(cal.get(ChineseCalendar.MONTH));
      System.out.println(cal.get(ChineseCalendar.DATE));
      System.out.println(cal.get(ChineseCalendar.IS_LEAP_MONTH));

            这样2011年6月6日就是农历的五月初五,并且五月不是闰月。

            农历转公历:

                SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
		Date date=df.parse("2011-5-5");
		ChineseCalendar cal=new ChineseCalendar(date);
		while(true){
			int month=cal.get(ChineseCalendar.MONTH);
			int day=cal.get(ChineseCalendar.DATE);
			if(month!=4||day!=5){
				cal.add(ChineseCalendar.DATE, 1);
				continue;
			}else{
				Calendar cal2=Calendar.getInstance();
				cal2.setTime(cal.getTime());
				System.out.println(cal2.get(Calendar.YEAR)+"-"+(cal2.get(Calendar.MONTH)+1)+"-"+cal2.get(Calendar.DATE));
				break;
			}
		}

           
这个算法有个假设,农历的月份和日期“落后”于公历,比如农历五月初五,那么它的公历比如是大于5月5日的,这个命题的证明比较简单:农历的春节(正月初
一)对应于公历日期的范围是1/21-2/21之间,也就是说公历比农历“早”至少"21天,公历每月最多31天,农历每月最少29天,如果农历要“赶
上”公历,那么至少得21/2=10.5个月,而且公历的二月只有最多29天,这样变成11.5个月,而且很多月(至少两个月)不是31天,这样累计下来
就永远“赶”不上了。当然证明的前提是“农历的春节(正月初一)对应于公历日期的范围是1/21-2/21之间”,这个命题可以参考[1],如果对这个算
法还不放心,那么可以双向找最近的那个农历日期。

             节气的计算:

             有很多节气也是节日,比如清明节,节气是公历,计算公式比较简单:参考http://baike.baidu.com/view/6385.htm#5

Java的Calendar

             Calendar是一个接口,获取Calendar实例的方法一般是 Calendar rightNow = Calendar.getInstance();

             Calendar的getInstance静态方法会根据本地的时区(TimeZone)和区域(Locale)选择合适的Calendar,比如我们这里,返回的其实是GregorianCalendar,因为中国官方的历法现在是日历。

              JDK1.6 getInstance()方法的会调用如下方法:

	private static Calendar createCalendar(TimeZone timezone, Locale locale) {
		if ("th".equals(locale.getLanguage())
				&& "TH".equals(locale.getCountry()))
			return new BuddhistCalendar(timezone, locale);
		if ("JP".equals(locale.getVariant())
				&& "JP".equals(locale.getCountry())
				&& "ja".equals(locale.getLanguage()))
			return new JapaneseImperialCalendar(timezone, locale);
		else
			return new GregorianCalendar(timezone, locale);
	}

               除了泰国和日本,其余都是  GregorianCalendar。中国现在官方的历法也是公历。

              泰国使用的佛历,其实基本就是公历,只不过纪年的开始不是基督耶稣的诞辰年,而是佛祖的诞辰年。
              日本的历法也是公历,不过纪年采用天皇的年号,这个比较麻烦,等现任天皇驾崩后又得维护数据了。

              Calendar的set方法会修改Fields(比如修改年月日等),但不会离开触发计算,必须调用get getTime等方法后才会现算。

        Calendar默认是“宽容”的,比如设置1月32日,不过出错,它会计算成2月1日。

       
一个月的第一周,很可能一个周分布在两个月中,这样可能导致一个月有5周,那么怎么样这个周算这个月
呢?getMinimalDaysInFirstWeek告诉我们,包含此周的天数如果大于等于这个值,那么这周就是这个月,默认是1,也就是说如果一个
周分布在两个月,那么这个周同时属于两个月。

        每周的第一天是哪天?getFirstDayOfWeek() 默认是Sunday,如果计算时想要符合中国人的习惯,那么需要设置这个值为Monday。

参考文献

        [1]  Helmer Aslaksen et al. The Mathematics of the Chinese Calendar  http://www.math.nus.edu.sg/aslaksen/calendar/cal.pdf

        [2] http://www.math.nus.edu.sg/aslaksen/calendar/chinese.shtml

        [3] 农历中闰年闰月的算法 http://conkeyn.iteye.com/blog/898304

        [4] 农历 http://baike.baidu.com/view/15163.htm

 

抱歉!评论已关闭.