农历开发笔记
0 概述
到2019年1月为止,关于农历的主题,github/PyPI 上已经有非常多的代码项目,语言有C、Java、Python等,具体的思路也不一样。综合来看,这些库有的功能单一,只覆盖某几个方面;有的已经很久没有更新了,主要是农历信息已在多年之前就采集完成,但是对于一些最新的数据修正未能及时涵盖;也有的在代码层面没有很好的适用最新的 Python 语言特性。
基于此,笔者利用收集整理的一些技术资料开发出了 Borax.Lunardate这个库,主要的目标和特点有:
- 完整的农历信息
在开发过程中笔者收集网络上的几个重要农历数据,包含了干支、生肖、节气等事项,并同时将它们作为数据验证的参考标准。
- 功能完备
Borax.Lunardate 库分为三个部分:1) 基于 LunarDate
的农历日期表示;2)类似于 datetime.strftime
的字符串格式系统;3) 一些常用的农历工具接口。
其中第2,3部分是网络上的农历库比较少涉及的,Borax-Lunardate 在这一方面非常有优势的。
- 对标datetime
在模块/类层面的组织和分类上,Borax.Lunardate 对标标准库的 datetime
和 calendar
模块,实现了这两个模块中与农历日期相联系的方法,LunarDate
和 date
类有许多相同的特性,包括不可变类、可比较性、时间加减等。 甚至有些命名也是一样的,比如 strftime
方法。
1 农历基本知识
农历是我国的传统历法,依据太阳和月球位置的精确预报以及约定的日期编排规则编排日期,并以传统命名方法表述日期。
2017年,我国已经颁布了国家推荐性标准 《GB/T 33661-2017 农历的编算和颁行》。
1.1 编排规则
农历属于一种阴阳合历,基本规则如下:其年份分为平年和闰年。平年为十二个月;闰年为十三个月。月份分为大月和小月,大月三十天,小月二十九天。一年中哪个月大,哪个月小,可由“置闰规则”计算决定。
若从某个农历十一月开始到下一个农历十一月(不含)之间有13个农历月,则需要置闰。置闰规则为:去其中最先出现的一个不包含中气的农历月为农历闰月。
除此之外,还有生肖纪年、干支纪年、二十四节气等。
1.2 表示方法
农历日期通常有以下几种表示方法:
- 农历乙未年正月初一
- 农历牛年闰五月十一
- 农历甲午年七月庚戌日
- 公元2016年农历丙申年十一月廿九
1.3 二十四节气
一个回归年内24个太阳地心视黄经等于15度的整数倍的时刻的总称,每个时刻成为一个节气。太阳每年运行360度,共经历二十四个节气,分别为立春(315度)、雨水(330度)、惊蛰(345度)、春分(0度、360度)、清明(15度)、谷雨(30度)、立夏(45度)、小满(60度)、芒种(75度)、夏至(90度)、小暑(105度)、大暑(120度)、立秋(135度)、处暑(150度)、白露(165度)、秋分(180度)、寒露(195度)、霜降(210度)、立冬(225度)、小雪(240度)、大雪(255度)、冬至(270度)、小寒(285度)、大寒(300度)。可以通过下面的儿歌记忆这些节气。
春雨惊春清谷天,
夏满芒夏暑相连,
秋处露秋寒霜降,
冬雪雪冬小大寒,
每月两节不变更,
最多相差一两天
2016年11月30日,中国“二十四节气”被正式列入联合国教科文组织人类非物质文化遗产代表作名录。
2 数据结构
2.1 大小月和闰月
从香港天文台网站可以获取1900 - 2100年的农历信息,每一天包含公历日期、农历日期、星期、节气四项基本信息。日期范围的基本信息如下表:
项目 | 起始日 | ... | 2100年 | 2101年 | ... | 截止日 |
---|---|---|---|---|---|---|
公历 | 1990年1月31日 | ... | 2100年12月31日 | 2101年1月1日 | ... | 2101年1月28日 |
农历 | 1900年正月初一 | ... | 2100年十二月初一 | 2100年十二月初二 | ... | 2100年十二月二十九 |
offset | 0 | ... | 73383 | 73384 | ... | 73411 |
干支 | 庚午年丙子月壬辰日 | ... | 庚申年戊子月丁未日 | - | ... | - |
具体到一个农历年中,从中可以看出以下几点信息:
- 每个月有多少天;哪些是大月(30天),哪些是小月(29天)
- 本年是否有闰月;如果有,是哪个月份
如何使用精炼的数据结构表述这些信息,是一个重要的前提,主要要求算法简单、内存占用少。网上有许多种方式,一种比较通行的做法是使用5字节的数据,高3位总是“000”,实际使用的低17位二进制。
字段 | 闰月大小标志 | 月份大小标志 | 闰月月份 |
---|---|---|---|
大小 | 4b | 12b | 4b |
2017年示例 | 0001 | 0101 0001 0111 | 0110 |
描述 | 本年有闰月 | 2,4,8,10,11,12为大月 | 六月是闰月 |
2019年示例 | 0000 | 1010 1001 0011 | 0000 |
描述 | 无闰月 | 1,3,5,8,11,12为大月 | 无闰月 |
综上所述,2017年信息可以使用 0x15176 表示;2019年信息可使用 0x0a930 表示。
2.2 节气的数据结构
二十四节气开始的日期,与通用的公历几乎一致,最多相差一两天,因为是按照地球一年绕太阳公转一周作为依据。比如小寒通常落在在1月5-7日,立春落在2月3-5日,冬至落在12月21-23日。即每个月都会有2个节气,1月只能有小寒、大寒这两个节气。
36位字符串表示法
构建两个含有24元素的数组,
第一个数组以小寒为第1个节气重新排列这24个节气。
小寒, 大寒, 立春, 雨水, 惊蛰, 春分, 清明, 谷雨, 立夏, 小满, 芒种, 夏至,
小暑, 大暑, 立秋, 处暑, 白露, 秋分, 寒露, 霜降, 立冬, 小雪, 大雪, 冬至
第二个数组表示对应节气对应的日期数字。
6 20 4 19 6 21 5 20 6 21 6 22 7 23 8 23 8 23 9 24 8 23 7 22
结合这两个数组,可记录在一个公历年中,二十四个节气分别是在哪一天。比如上述的24个数字可解释为:1月6日是小寒、1月20日是大寒...12月7日是大雪、12月22日是冬至。
在 Python 语言层面,可以使用字符串(基本数据类型)代替上述数组(复合数据类型),即"620419621520621622723823823924823722"
,需要36位字符存储。
解析表中数据的 Python 代码实现如下:
def parse_term(year_info):
result = []
for i in range(0, 36, 3):
s = year_info[i:i + 3]
result.extend([int(s[0]), int(s[1:3])])
return result
30位字符串表示法
jjonline/calendar.js 提供了一种用更为简单的表示方法:利用十六进制压缩数字的位数,进一步简化为30位的字符串。具体计算过程如下:
9778397bd097c36b0b6fc9274c91aa # 按长度5分割,共6组
97783 97bd0 97c36 b0b6f c9274 c91aa # 转化为十进制
620419 621520 621622 723823 823924 823722 # 按长度1,2,1,2细分
6 20 4 19 6 21 5 20 6 21 6 22 7 23 8 23 8 23 9 24 8 23 7 22
使用 Python代码实现上述算法如下:
def parse_term(term_info):
values = [str(int(term_info[i:i + 5], 16)) for i in range(0, 30, 5)]
term_day_list = []
for v in values:
term_day_list.extend([
int(v[0]), int(v[1:3]), int(v[3]), int(v[4:6])
])
return term_day_list
24位字符串
从 Borax v1.2.0 开始使用算法。
统计1900-2100年之前节气日期统计可知,中气的日期都是在18-24日之间,这些均为两位数,可以通过线性变化转为一位数的数字,结合月份特点,可以通过减去一个固定偏移量15就是比较好的选择。
同样的按照上述处理,具体过程如下:
654466556667788888998877 # 按长度1分割
6 5 4 4 6 6 5 5 6 6 6 7 7 8 8 8 8 8 9 9 8 8 7 7 # 增加偏移量,奇位置为0,偶位置为15
6 20 4 19 6 21 5 20 6 21 6 22 7 23 8 23 8 23 9 24 8 23 7 22
同样的使用Python 代码如下,和30位表示法相比,更为简单直接。
def parse_term_days(term_info):
return [int(c) + [0, 15][i % 2] for i, c in enumerate(term_info)]
3 数据处理
本模块的数据和算法参考自项目 jjonline/calendar.js
v1.2 发布后,将针对原始数据进行一次校验
提供了数据源,说明
- 微软数据源比对使用于项目 Lunar-Solar-Calendar-Converter 的相关代码
月份 | Borax-v1.2 | 微软农历 | 香港天文台 | 初步操作 | |
---|---|---|---|---|---|
1933年闰5月 | 29 | 30 | 30 | 改(0x6e95 -0x16a95) | |
1933年6月 | 30 | 29 | 29 | 改() | |
1996年5月 | 29 | 30 | 30 | 改(0x055c0 - 0x05ac0) | |
1996年6月 | 30 | 29 | 29 | 改 | |
1996年7月 | 29 | 30 | 30 | 改 | |
1996年8月 | 30 | 29 | 29 | 改 | |
2060年3月 | 30 | 29 | 29 | 改(0x0a2e0 - 0x092e0) | |
2060年4月 | 29 | 30 | 30 | 改 | |
2089年7月 | 29 | 30 | 29 | 0x0d160 (0x0d260) | |
2089年8月 | 30 | 29 | 30 | ||
2097年6月 | 29 | 30 | 29 | 0x0a2d0 (0x0a4d0) | |
2097年7月 | 30 | 29 | 30 |