Python公历转换农历及简易万年历

6 篇文章 3 订阅
2 篇文章 2 订阅

一般使用的万年历,只提供距今前后百年的日历。这是因为其所用的计算方法是一种简便的近似计算,适用范围较小。其次,天文学方法计算量大,不适合日常软件使用。但如果要进行历史研究,范围就超出常用日历,本文即实现计算任意日期的农历。

由于天文星历数据太大,本文的代码是直接利用python的PyEphem库。该库只提供了二分二至时间,没有24节气。但是也提供了任意时间的太阳黄经,可以利用此项计算24节气。

本文提供三个功能:PerpetualCalendar万年历,Solar2LunarCalendar公历转农历,Lunar2SolarCalendar农历转公历

中国阴阳历基础:

1. 以太阳历确定岁首,方法是通过测量影长确定冬至。以冬至所在月为子月,其后每月依次排序,闰月无建。

2. 现行农历用寅正,即冬至后第二个月(冬至月起的第三月),冬至固定在十一月。

3. 将二十四节气中的偶数序的节气称为十二中气。每月对应一个中气,若无中气,则该月作为闰月。

4. 同时定气和定朔的情况下,可能出现多个无中气月,如此则规定只有连续两个冬至月之间出现十三次合朔年份的的第一个无中气月置闰。

5. 使用节气月排定月份干支。

详细农历分析另见农历计算方法一文。

本文提供的简易万年历直接控制台打印:

简易万年历

 如需下图所示UI程序,另见:Python万年历(含农历、节气等)

 

import math, ephem

yuefen =["正月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]
nlrq = ["初一","初二","初三","初四","初五","初六","初七","初八","初九","初十","十一","十二","十三","十四","十五","十六","十七","十八","十九","二十","廿一","廿二","廿三","廿四","廿五","廿六","廿七","廿八","廿九","三十"]
tiangan = ["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"]
dizhi = ["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"]
gz = [''] * 60  # 六十甲子表
for i in range(60):
	gz[i] = tiangan[i % 10] + dizhi[i % 12]

def JD2date(JD, ut=0):
	return ephem.Date(JD + ut/24 - 2415020)

def EquinoxSolsticeJD(year, angle):
	if 0 <= angle < 90:
		date = ephem.next_vernal_equinox(year)
	elif 90 <= angle < 180:
		date = ephem.next_summer_solstice(year)
	elif 180 <= angle < 270:
		date = ephem.next_autumn_equinox(year)
	else:
		date = ephem.next_winter_solstice(year)
	JD = ephem.julian_date(date)
	return JD

# 计算二十四节气
def SolarLongitube(JD):
	date = JD2date(JD)
	s = ephem.Sun(date)  # date应为UT时间
	sa = ephem.Equatorial(s.ra, s.dec, epoch=date)
	se = ephem.Ecliptic(sa)
	L = se.lon / ephem.degree / 180 * math.pi
	return L

def SolarTerms(year, angle):
	if angle > 270: year -= 1  # 岁首冬至
	if year == 0: year -= 1  # 公元0改为公元前1
	JD = EquinoxSolsticeJD(str(year), angle)  # 初值
	if angle >= 270:
		JD0 = EquinoxSolsticeJD(str(year), (angle - 90) % 360)
		if JD < JD0: # 非年末冬至
			JD = EquinoxSolsticeJD(str(year+1), angle)  # 转入次年
	JD1 = JD
	while True:
		JD2 = JD1
		L = SolarLongitube(JD2)
		JD1 += math.sin(angle * math.pi / 180 - L) / math.pi * 180
		if abs(JD1 - JD2) < 0.00001:
			break  # 精度小于1 second
	return JD1  # UT

def DateDiffer(JD1, JD2):
	return math.floor(JD1 + 8 / 24 + 0.5) - math.floor(JD2 + 8 / 24 + 0.5)

def DateCompare(JD1, JD2): # 输入ut,返回ut+8的比较结果
	if DateDiffer(JD1, JD2) >= 0: return True  # JD1 >= JD 2
	else: return False

def findSZY(JD, shuoJD):  # 查找JD所在的农历月份
	szy = -1
	for i in range(len(shuoJD)):
		if DateCompare(JD, shuoJD[i]):
			szy += 1  # date所在的阴历月序,起冬至朔
	return szy

def findDZS(year): # 寻找年前冬至月朔日
	if year == 1: year -= 1  # 公元元年前冬至在公元前1年
	dz = ephem.next_solstice((year - 1, 12)) # 年前冬至
	jd = ephem.julian_date(dz)
	# 可能的三种朔日
	date1 = ephem.next_new_moon(JD2date(jd - 0))
	jd1 = ephem.julian_date(date1)
	date2 = ephem.next_new_moon(JD2date(jd - 29))
	jd2 = ephem.julian_date(date2)
	date3 = ephem.next_new_moon(JD2date(jd - 31))
	jd3 = ephem.julian_date(date3)
	if DateCompare(jd, jd1): # 冬至合朔在同一日或下月
		return date1
	elif DateCompare(jd, jd2) and (not DateCompare(jd, jd1)):
		return date2
	elif DateCompare(jd, jd3): # 冬至在上月
		return date3

def LunarCalendar(nian, type=1):   # type=1时截止到次年冬至朔,=0时截止到次年冬至朔次月
	dzs = findDZS(nian)
	shuo = dzs  # 计算用朔,date格式
	shuoJD = [ephem.julian_date(dzs)]  # 存储ut+8 JD,起冬至朔
	next_dzsJD = ephem.julian_date(findDZS(nian + 1))  # 次年冬至朔
	i = -1  # 中气序,从0起计
	j = -1  # 计算连续两个冬至月中的合朔次数,从0起计
	zry = 0
	flag = False
	# 查找所在月及判断置闰
	while not DateCompare(shuoJD[j+type], next_dzsJD):  # 从冬至月起查找,截止到次年冬至朔
		i += 1
		j += 1
		shuo = ephem.next_new_moon(shuo)  # 次月朔
		shuoJD.append(ephem.julian_date(shuo))
		# 查找本月中气,若无则置闰
		if j == 0: continue  # 冬至月一定含中气,从次月开始查找
		angle = (-90 + 30 * i) % 360  # 本月应含中气,起冬至
		qJD = SolarTerms(nian, angle)
		# 不判断气在上月而后气在后月的情况,该月起的合朔次数不超过气数,可省去
		if DateCompare(qJD, shuoJD[j+1]) and flag == False:  # 中气在次月,则本月无中气
				zry = j + 1  # 置闰月
				i -= 1
				flag = True  # 仅第一个无中气月置闰
	# 生成农历月序表
	ymb = []
	for k in range(len(shuoJD)):
		ymb.append(yuefen[(k - 2) % 12])  # 默认月序
		if j + type == 13:  # 仅12次合朔不闰,有闰时修改月名
			if k + 1 == zry:
				ymb[k] = '闰' + yuefen[(k-1 - 2) % 12]
			elif k + 1 > zry:
				ymb[k] = yuefen[(k-1 - 2) % 12]
	return ymb, shuoJD   # 月名表,合朔JD日期表


def PerpetualCalendar(year, month=0):  # 万年历(农历及公历对照),不指定月份时输出全年
	if year == 0: return print('不存在公元0年')
	ymb, shuoJD = LunarCalendar(year, 0)
	if DateCompare(ephem.julian_date((year, 12, 31)), shuoJD[-2] + 29):
		ymb1, shuoJD1 = LunarCalendar(year+1)
		ymb = ymb[:-2] + ymb1[:2]
		shuoJD = shuoJD[:-2] + shuoJD1[:3]
	days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
	days[1] = 29 if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 else 28
	if year < 1582 and year % 4 == 0: days[1] = 29
	week = ['一', '二', '三', '四', '五', '六', '日', ]
	for j in range(12):
		if month != 0 and j + 1 != month: continue
		print('【' + str(year) + '年 ' + str(j+1) + '月  日历】')
		ysJD = ephem.julian_date((year, j+1))
		szy = findSZY(ysJD, shuoJD)  # 公历岁首对应的农历月
		ysRQ = DateDiffer(ysJD, shuoJD[szy])  # 每月1日的农历日期
		yue0 = DateDiffer(shuoJD[szy + 1], shuoJD[szy])
		yue1 = DateDiffer(shuoJD[szy + 2], shuoJD[szy + 1])
		blank = int((ysJD + 0.5) % 7)
		flag = False
		for row in range(6*2+1):
			if row % 2 == 1 and flag: break
			for k in range(7):
				if row % 2 == 1:  # 公历行
					day = row // 2 * 7 + k - blank + 1
					if year == 1582 and j == 9:
						if day > 4: day += 10
				elif row != 0:  # 农历行
					if row == 2 and k >= blank or row > 2:
						rqx = ysRQ + row // 2 * 7 - 7 + k - blank
						if rqx == 0: rq = ymb[szy]
						elif 0 < rqx < yue0: rq = nlrq[rqx]
						elif rqx == yue0: rq = ymb[szy+1]
						elif yue0 < rqx < yue0 + yue1: rq = nlrq[rqx - yue0]
						elif rqx == yue0 + yue1: rq = ymb[szy + 2]
						elif rqx > yue0 + yue1: rq = nlrq[rqx - yue0 - yue1]
				# 输出排版
				if row == 0: print(" {:<5}".format(week[k]), end='')
				elif row == 1 or row == 2:  # 首行
					if k == 0:
						print('       ' * blank, end='')
					if row == 1 and k >= blank:
						print(" {:<6d}".format(day), end='')
					if row == 2 and k >= blank:
						print("{0:{1}<3}".format(rq, '\u3000'), end=' ')
				else:
					if row % 2 == 1 and row != 1:
						if day <= days[j]:
							print(" {:<6d}".format(day), end='')
						else:
							flag = True
							break
					if row % 2 == 0 and row != 0 and row != 2:
						if year == 1582 and j == 9 and day > days[j]: break
						print("{0:{1}<3}".format(rq, '\u3000'), end=' ')
						if row // 2 * 7 + k - blank - 6 >= days[j]: break
			print()
		print()

def Solar2LunarCalendar(date): # 默认输入ut+8时间
	if date[0] == '0': return '不存在公元0年'
	JD = ephem.julian_date(date) - 8/24 # ut
	year, month, day = JD2date(JD, 8).triple()
	# 判断所在年
	dzs = findDZS(year)  # 本年冬至朔
	next_dzs = findDZS(year+1)  # 次年冬至朔
	this_dzsJD = ephem.julian_date(dzs)
	next_dzsJD = ephem.julian_date(next_dzs)
	nian = year  # 农历年
	if DateCompare(JD, next_dzsJD):  # 该日在次年
		nian += 1
	if not DateCompare(JD, this_dzsJD):  # 该日在上年
		nian -= 1
	# 判断所在月
	ymb, shuoJD = LunarCalendar(nian)
	szy = findSZY(JD, shuoJD)
	# 判断节气月
	if year < 0: year += 1
	jqy, jqr = JD2date(SolarTerms(year, month*30 + 255), 8).triple()[1:]
	if int(jqy) != month: month -= (int(jqy) - month)
	if day >= int(jqr): ygz = gz[(year * 12 + 12 + month) % 60]
	else: ygz = gz[(year * 12 + 11 + month) % 60]
	# 以正月开始的年干支
	if szy < 3: nian -= 1  # 正月前属上年
	if nian < 0: nian += 1
	ngz = gz[(nian - 4) % 60]
	rgz = gz[math.floor(JD + 8/24 + 0.5 + 49) % 60]
	rq = DateDiffer(JD, shuoJD[szy])  # 月内日期
	return date + ' 为农历:' + ngz + '年 ' + ygz + '月 ' + rgz + '日 ' + ymb[szy] + nlrq[rq] + '\n'


def Lunar2SolarCalendar(nian, date):  # 正月开始的年
	date1 = date.split('闰')[-1]
	year = nian
	yx = yuefen.index(date1[:-2])
	if yx + 1 > 10: year += 1  # 计算用年,起冬至朔
	if year == 0: return '不存在公元0年'
	yx = (yx + 2) % 12  # 子正转为寅正
	if "闰" in date: yx += 1
	# 查找所在月
	ymb, shuoJD = LunarCalendar(year, 0)
	szy = 0
	for i in range(len(ymb)):
		if ymb[i] == date1[:-2]:  # 按月序查找
			if ymb[i + 1] == date[:-2] or '闰' in date:
				szy += 1  # 可能为闰月(不闰则计算次月)
			break
		szy += 1
	# 获得农历日期
	try:
		rq = nlrq.index(date[-2:])
	except:
		rgz = gz.index(date[-2:])
		sgz = math.floor(shuoJD[szy] + 8/24 + 0.5 + 49) % 60
		rq = (rgz - sgz) % 60
		if DateCompare(shuoJD[szy] + rq, shuoJD[szy+1]):
			print('该月无' + date[-2:])
		else:
			print(date[-2:] + '为该月' + nlrq[rq] + '日')
	date2 = str(JD2date(shuoJD[szy] + rq, 8))[:-9]
	return '农历' + str(nian) + '年' + date + ' 为公历:' + date2


PerpetualCalendar(2020)

#date = input('请输入日期:')
date = "2019-3-15"
date1 = "2016-11-29"
date2 = "2033-9-1"      # 无中气月,一年仅得12月,不闰
date3 = "2033-12-31"    # 冬至起的第一个无中气月,闰
date4 = "2034-3-1"      # 冬至起的第二个无中气月,不闰
print(Solar2LunarCalendar(date))

print(Lunar2SolarCalendar(-1696, '十一月甲子'))
print(Solar2LunarCalendar('-1695/1/7'))
print(Lunar2SolarCalendar(2020, '正月初一'))
print(Lunar2SolarCalendar(2033, '闰十一月十一'))

天文计算中经常简化历表以简便计算,简化会降低计算精度。特别是时间进位可能会差到一天。计算定气定朔,日期越久远误差越大。如-2/12/26定朔23:59:38,若误差约1分钟,则可能定朔为12/27日。不过这种情况很少见。

本程序虽以ephem库为例,但在农历算法上使用一般化的方法使其适用于较宽的年限以便适应不同精度的历表要求。实际上,ephem库计算月亮位置的精度有限,适用年限也不广。在较小应用的范围内,可根据一些天文学特征简化程序。例如,如果上限不超过BC1100年,则无需考虑岁差和不同历法对回归年的取值问题,导致冬至计算结果在次年的情况(此后冬至固定在12月)。如果下限也不远,则无需考虑地球自转变慢和近点进动,计算节气利用太阳在近日点和远日点不同的运动特点,平气法寻找较好的值(如31)可以保证定气不会超过平气计算结果从而仅需比较一次即可获得真实节气。

  • 11
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
好的,我知道了。您可以采用 Python 的 calendar 模块以及一些公式来实现含农历、节气的万年历程序。以下是一个简单的示例代码: ```python import calendar from datetime import datetime # 定义节气名称 JIE_QI = ['大寒', '雨水', '春分', '谷雨', '夏至', '大暑', '秋分', '霜降', '冬至', '小寒', '立春', '惊蛰'] # 定义农历日期名称 LUNAR_DAY = ['朔', '初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十'] # 定义农历月份名称 LUNAR_MONTH = ['正月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '冬月', '腊月'] # 计算某年某月有多少天 def get_month_days(year, month): return calendar.monthrange(year, month)[1] # 计算某年某月的节气 def get_jieqi(year, month): res = [] for i in range(24): jieqi = calendar._julian_day_to_datetime(calendar._jieqi_before_solstice(year, i)) if jieqi.month == month: res.append(f'{JIE_QI[i]}({jieqi.day}日)') return res # 计算某年某月的闰月 def get_leap_month(year): return calendar.leapdays(1900, year) # 计算某年某月的农历日期 def get_lunar_calendar(year, month): res = [] lunar_calendar = calendar.chinese_calendar(year) _, leap_month, leap_day = lunar_calendar[0] for day in lunar_calendar[month]: if day == leap_day and month == leap_month: res.append('闰' + LUNAR_MONTH[month - 1]) else: res.append(LUNAR_DAY[day - 1]) return res # 输出某一天的万年历信息 def print_calendar(year, month, day): print(f'{year}年{month}月{day}日') print('星期' + '日一二三四五六'[calendar.weekday(year, month, day)]) print('节气: ' + ', '.join(get_jieqi(year, month))) print('农历: ' + ''.join(get_lunar_calendar(year, month)[day - 1])) # 测试代码 today = datetime.today().date() print_calendar(today.year, today.month, today.day) ``` 注意,上述代码仅为示例,实际应用中需要进行更多的优化和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值