从百度上面考下来的一个农历日起算法。。。。。。。。
Python实现150年公历农历换算
烤鱼片
前段时间玩twitter,跟风做了一个自动报时的机器人。一来练习python,而来玩玩gae,看看twitter的api是什么样子的。单是报公历太普通不好玩,于是想来做个报农历时间的,于是研究了一下农历的换算问题,并用python做了个小程序实现从1901年到2050年的换算。
要实现这个功能首先是要搞明白农历历法的规律。以前没查过这方面材料,以为是有什么公式可以直接从公历时间换算一下就可以,其实不然,中国农历的推算那可是相当复杂的,精确计算是得用上天体力学的。
首先要纠正一个不准确的说法,就是阴历。我们用的农历历法民间也俗称阴历,对应公元纪年的叫做阳历。顾名思义,阳历就是根据太阳与地球的相对运动关系计算的立法,阴历则是根据月亮周期计算的。乍看我们的农历好似是阴历,一个月恰是一个月亮的周期,但实际上农历还要根据太阳周期对一年的日期进行矫正,否则要是简单按照月亮周期计算的话一年就不是365天,会跟阳历有固定的误差,时间久了误差就会越来越大。为了避免这种误差,保证阴历一年和阳历一年基本对应,就引入了矫正机制,也就是闰月。而24节气虽是农历节气,但其时间则是根据地球相对太阳公转的相对位置确定的,而不是固定在某个阴历日期。因此我们所说的阴历其实应该算是一种阴阳混合历,真正典型的阴历代表则是伊斯兰历。
农历的计算就要涉及到每年是否闰月,闰几月,每个月有多少天,也就是大小月问题,二十四节气日期的确定这几个关键数据,这些都要用到太阳月亮地球三者之间的运行状况的计算,非专业人士很难搞的明白,而作为一般应用,也只有一种比较好的办法,那就是查表,找到一些关键时间数据存下来,计算的时候根据这些数据进行计算。
那么,公历到农历的转化需要最终得到哪些信息呢?首先第一个是农历的年份,也就是每年哪一天是大年初一,这个日子的确定取决于每年的闰月情况和各个月的大小月分布;然后是农历的月份和日期,也就是要算出今天是农历的几月几号;所计算日期是否属于24节气,是哪个节气;以及年干支、月干支、日干支乃至某时刻的干支是什么都要算出来。
我们先从简单的开始算起。
1.时刻干支
时刻干支简单的将就是在古装剧上常听到“子时”“午时”来表示一天的时刻,12个地支一天,也就是一个地支是两个小时,子时就是从23点到第二天的1点间俩小时,其他的类推下去。如果再精确点还可以在地支前加一个天干,也就是每个时刻可以表示成“甲子”“乙丑”这样的。地支时刻很简单,就是一天对应12个,直接对应就行。天干是10个,和地支组合起来则是60一循环,不能跟一天的12个时辰直接对应,所以就要取一个基准时刻,知道那个时刻的天干地支就可以根据60一循环的规律进行推算了。在这里我以1901年的1月1日凌成1点为基准点,此刻是乙丑时的开始,函数只做了向后计算,向前推算也很简单,稍改一下就可以。这里为最终返回干支结果,定义了几个常量,对应干支的汉字。
TIAN_GAN=("甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸")
DI_ZHI=("子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥")
HOUR_GANZHI={0:"子时", 1:"丑时", 2:"丑时", 3:"寅时", 4:"寅时", 5:"卯时", 6:"卯时", 7:"辰时", 8:"辰时", 9:"巳时", 10:"巳时", 11:"午时"\
, 12:"午时", 13:"未时", 14:"未时", 15:"申时", 16:"申时", 17:"酉时", 18:"酉时", 19:"戌时", 20:"戌时", 21:"亥时", 22:"亥时", 23:"子时", 24:"子时"}
def GetHourGanzhi1901(arg):
#以1901年1月1日凌晨一点为基准点
startdatetime=datetime.datetime(year=1901, month=1, day=1, hour=1)
#60干支乙丑是第二个,以0为起点,则编号为1
startganzhi=1
if not isinstance(arg, datetime.datetime):
return ""
#计算离基准时刻过去了多少时间
delta = arg - startdatetime
if delta.seconds<0:
return ""
#计算时刻的干支编号
hour=delta.days*24+delta.seconds/3600
num=(startganzhi+hour/2)%10
return TIAN_GAN[num]+HOUR_GANZHI[arg.hour]
2.日干支
日干支就是每天对应的天干地支表示,基本做法与时刻干支类似,也是以某一确定基准日期为起点,根据时间差来计算。这里同样也是以1901年1月1日为基准点,当天是乙卯日。
def GetDayGanzhiNum1901(arg):
#函数返回60干支编号,编号范围[1,60]
if not isinstance(arg, datetime.date):
return 0
startdate=datetime.date(1901,1,1)
startganzhi=16
if not isinstance(arg, datetime.date):
return 0
delta = arg - startdate
if delta.days + delta.seconds<0:
return ""
num=(startganzhi+delta.days)%60
if num == 0:
num=60
return num
获得编号后如需获取汉字表示,可以再写一个函数来做这种转换:
def Num2Ganzhi(arg):
#arg范围是[1,60]
if not arg in range(1, 61):
return ""
x=(arg-1)%10
y=(arg-1)%12
return TIAN_GAN[x]+DI_ZHI[y]
3.农历年份
农历年份的确定实际上就是要设法知道每年年初一在公历的哪天,上面提到这涉及到每年的闰月和大小月分布,如果不用复杂的天文计算的话只能是用查表的方式解决。我用的数据是从网上搜来的代码里的,是一位信箱是[email protected]的朋友整理出来的,数据时间从1901年到2050年,共150年,代码中原作者表示如需使用他整理出来的数据和代码的话须注明这一情况,本着尊重原作者劳动的原则,在此向此为朋友表示感谢。
原作者代码是用于嵌入式设备的,所以对数据采取了特别的存储方式以减小内存使用量。比如说从1901到1910年的闰月的月份采取如下存储方式,如没有闰月则为0,每字节存两年。0x00, 0x50, 0x04, 0x00, 0x20, #1910,这样不直观,所以我对这种数据做了展开处理,成为1901:0, 1902:0如此形式的数据,表示每一年闰月月份,更加直观,易于阅读。
为了计算农历年份,我做了一个150年间每年正月初一对应与阳历年第几天的表。
YEAR_FIRST_DAY={1901:50, 1902:39, 1903:29, 1904:47, ...}
借用此表就可以计算此150年间每一天对应是哪一个农历年份了。
为了方便管理,以下的功能都写到一个ChineseCalendar150类中。其中获取农历年份的函数部分代码如下所示:
def __GetChineseYearNum(self, arg=None):
"""将公立日期转换为农历年份"""
#计算是这一年的第几天
lastyear1231=datetime.date(arg.year-1,month=12,day=31)
delta=arg-lastyear1231
year=1901
if delta.days>=self.YEAR_FIRST_DAY[arg.year]:
year=arg.year
else:
year=arg.year-1
return year
4.农历日期
农历日期涉及因素比较多一点,计算这个首先需要农历年份,这点好办,上一步已经做出来了,然后还需要当年闰月情况,以及每个月的大小月情况。大小月指的是农历月有的是30天,有的是29天,这个天数不像公历那么固定有规律,它是为了修正月亮周期不能整除地球天而来的,所以得用对应表记下来。
MONTH_DAY={1901:[29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29, 0],...}
每年用13个数字记录每月的天数,最后一个是0表明当年没闰月。
闰月情况也同样是要建表存储。
RUN_YUE={1901:0, 1902:0, 1903:5, 1904:0, 1905:0, 1906:4, 1907:0, 1908:0, 1909:2, 1910:0,...}
每年年号对应数字为闰月月份,0表示当年没有闰月。
这样,只要通过公历日期就可以得到所属农历年份,通过农历年份就知道年初一是公历哪一天,进而得知当天是农历年中的第几天,然后再根据当年闰月及大小月情况推算出当日是农历的哪一月的第几天了。
def __GetChineseDate(self,arg=None):
"""获取某日期的农历日期,返回(是否大月,是否闰月,月份,日子)"""
dayue={30:True,29:False}
#获取农历年份
yearnum=self.GetChineseYearNum(arg)
#计算当天是农历年的第几天
delta=datetime.timedelta(days=self.YEAR_FIRST_DAY[yearnum]-1)
gongliyuandan=datetime.date(year=yearnum,month=1,day=1)
gonglichiyi=gongliyuandan+delta
delta=arg-gonglichiyi
days=delta.days+1
month=[]
if self.RUN_YUE[yearnum]>0:
for i in range(13):
if i+1<=self.RUN_YUE[yearnum]:
month.append([False,i+1,self.MONTH_DAY[yearnum][i]])
elif i==self.RUN_YUE[yearnum]:
month.append([True,i,self.MONTH_DAY[yearnum][i]])
else:
month.append([False,i,self.MONTH_DAY[yearnum][i]])
pass
else:
for i in range(12):
month.append([False,i+1,self.MONTH_DAY[yearnum][i]])
for i in range(len(month)):
days-=month[i][2]
if days<=0:
return (dayue[month[i][2]],month[i][0],month[i][1],days+month[i][2])
5.年干支
年干支就是用天干地支来标记年份,比如2010年就是庚寅虎年,天干地支60年一轮回不停的循环,进而年干支的计算也就还是找个基准点,计算相差年份。但这里有个问题,就是年干支的更替时刻是什么时候。现在我们一般是将这个时刻定在农历正月初一的凌成0时,也就是春晚敲钟的那一刻。这一刻起,农历年干支就要更换,进而属相也就更换。但实际上在过去传统立法中是以立春算起的,或者更严格来讲是从立春的那一时刻开始更替年干支的。这样就会出现一个有意思的情况,就是如果两个人出生在同一年的立春那天,但是一个是在立春那刻前出生,另一个在之后出生,他们按照传统历法虽然同一天出生,但属相却不同。(说到这,我想到一个问题,那就是可能很多朋友搞不清楚属相怎么算,还以为是从元旦那天所起,那更是大错特错了)我们就不那么考究了,不去考虑那么精确的时刻问题,还是以天为整体单位吧,首先按照正月初一更换年干支来计算,后面再补充一个扩展类来实现立春更换年干支功能。(其实初一更换年干支也有点问题,那就是到底是从零点算起呢还是从子时也就是前一天23点算起呢,呵呵,不考虑那么多了)
我们的基准年份是1901年,当年大年初一是公历的2月19日,部分代码如下。
def __GetYearGanzhiNum(self,arg=None):
"""获取干支年序号[1,60]"""
year=self.GetChineseYearNum(arg)
num=((year-self.START_DATE.year)+Ganzhi2Num(self.START_YEAR_GAN_ZHI))%60
if num ==0:
num=60
return num
6.月干支
月干支情况与年干支类似,也是循环更替,也有月干支更替时刻的问题。现在一般简单的就是每月初一更替,但传统上也是要根据24节气时刻计算的,也就是每隔两个节气是一个月。首先在这个类中还是以初一为界进行计算,不过要注意的是,以月初一为界的情况下认为闰月是不更换月干支的。
基准为1901年农历一月,庚寅月,部分代码如下。
def __GetMonthGanzhiNum(self,arg=None):
"""获取某日期的月干支序号,[1,60]"""
chineseyear=self.GetChineseYearNum(arg)
chinesedate=self.GetChineseDate(arg)
startchinesedate=self.GetChineseDate(self.START_DATE)
years=chineseyear-self.START_YEAR
months=years*12+chinesedate[2]-startchinesedate[2]
ganzhinum=months+Ganzhi2Num(self.START_MONTH_GAN_ZHI)
ganzhinum=ganzhinum%60
if ganzhinum==0:
ganzhinum=60
return ganzhinum
7.节气
24节气是根据地球绕太阳运行位置确定的,所以跟农历肯定是没有固定对应关系,同时公历也有一年不是正好365天的误差问题(闰年),所以24节气在公历中也不是固定日子,但好处是相对固定,总是在那么前后一两天浮动,而且24节气均匀分布在12个公历月份中,每月俩,这样就可以制作这样的一个表。
JIE_QI={1901:(6,21,4,19,6,21,5,21,6,22,6,22,8,23,8,24,8,24,9,24,8,23,8,22),...}
每年用一个24元素的数组,每两个标明对应公历月内两个节气的日期就可以了。
def __GetJieQi(self,arg=None):
"""获取节气"""
year=self.JIE_QI[arg.year]
if year[(arg.month-1)*2]==arg.day:
return JIE_QI_NAME[(arg.month-1)*2]
elif year[(arg.month-1)*2+1]==arg.day:
return JIE_QI_NAME[(arg.month-1)*2+1]
return ""
8.年干支-立春版
上面说了,年干支的更替有不同的方式,上面的是以正月初一为界,现在我们在来实现一个以立春为界的。为此我在这里新写了一个继承自ChineseCalendar150的子类,叫ChineseCalendar150A。参照ChineseCalendar150中计算年干支的方法,只要能以某一年为基点,计算出当前所属年与基准年的差就能知道现在的干支了。但这里的这个年份要从立春而不是年初一算起。这样我们需要另写一个计算年号的函数,但是又不能覆盖掉父类的,否则会影响集成自父类中其他几个用到年号函数的计算,所以新写一个名为__GetChineseYearNumA。基本方法与父类中的类似,只是把存储当年正月初一在当年公历第几天的地方换成了立春在当年公历中的第几天。
def __GetChineseYearNumA(self, arg=None):
"""将公立日期转换为农历年份,以立春为界"""
lastyear1231=datetime.date(arg.year-1,month=12,day=31)
delta=arg-lastyear1231
year=1901
#每年立春固定在公历2月的第一个节气
if delta.days>=self.JIE_QI[arg.year][2]+31:
year=arg.year
else:
year=arg.year-1
return year
然后就可以__GetChineseYearNumA为基础计算年干支了。这里采取了覆盖父类函数的方法。
def __GetYearGanzhiNum(self,arg=None):
"""获取某日期的干支年数字[1,60],立春为界"""
year=self.GetChineseYearNumA(arg)
num=((year-self.START_DATE.year)+Ganzhi2Num(self.START_YEAR_GAN_ZHI))%60
if num ==0:
num=60
return num
9.月干支-节气版
以节气为界的月干支计算也是以某个起始月为基准,此处以农历1901年立春开始为基准。
def __GetMonthGanzhiNum(self,arg=None):
"""获取某日期的月干支序号,[1,60],24节气为界"""
chineseyear=self.GetChineseYearNumA(arg)
#计算当前日期在以24节气为界限的划分中所处的位置,推算出在以24节气划分的12个月份中是哪一月
a=self.JIE_QI[arg.year][(arg.month-1)*2]
if arg.day
thismonth=arg.month-2
else:
thismonth=arg.month-1
if thismonth==-1:
thismonth=11
elif thismonth==0:
thismonth=12
#计算与基准月份相距的月数
years=chineseyear-self.START_YEAR
months=years*12+thismonth-1
ganzhinum=months+Ganzhi2Num(self.START_MONTH_GAN_ZHI)
ganzhinum=ganzhinum%60
if ganzhinum==0:
ganzhinum=60
return ganzhinum
基本功能就是上面这些,附件中有完整的代码文件,其中还包含了一些最后没用到的数据表和函数,我也懒得整理。我没有做太仔细的测试验证,只是随便跳了几个日子用《日梭万年历》对照了一下没出错。代码中的基准数据也是从《日梭万年历》上查来的。程序纯属学习研究娱乐而做,难保不会有什么错误,各位朋友大可拿去用,出了问题别来骂我就好。如果觉得有什么地方做的不好敬请发信留言指教。
twitter上做了个用这个程序写的自动报时机器人,基本上还算工作正常,感兴趣的可以去fo,http://twitter.com/ganzhitime。我在twitter上的大号http://twitter.com/iakula 也欢迎来fo。