当前位置:   article > 正文

Borax.Lunardate:中国农历日期

lunardate

原文地址:https://kinegratii.github.io/2019/01/05/lunardate-module/

感谢原作者!本人只是搬运工。看完这个和上一篇基本对农历就有了一个较全面的认识。

本文简要介绍了我国传统的农历历法知识,并叙述了 Borax-Lunar 工具库开发背后的一些算法原理和技术资料。

目录

1 农历概述

1.1 编排规则

1.2 表示方法

1.3 二十四节气

2 数据结构

2.1 大小月和闰月

2.2 节气的数据结构

3 Borax-Lunardate概述

4 模块设计

4.1 LunarDate日期类

4.1.1 初始化日期对象

4.1.2 基准日期

4.2 格式化显示

4.2.1 使用方法

4.2.2 源码解析

4.3 类型标注

4.3.1 概述

4.3.2 常用的使用示例

5 参考资料


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度)。可以通过下面的儿歌记忆这些节气。

 

  1. 春雨惊春清谷天,
  2. 夏满芒夏暑相连,
  3. 秋处露秋寒霜降,
  4. 冬雪雪冬小大寒,
  5. 每月两节不变更,
  6. 最多相差一两天

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年十二月二十九
offset0733837338473411
干支庚午年丙子月壬辰日庚申年戊子月丁未日--

具体到一个农历年中,从中可以看出以下几点信息:

  • 每个月有多少天;哪些是大月(30天),哪些是小月(29天)
  • 本年是否有闰月;如果有,是哪个月份

如何使用精炼的数据结构表述这些信息,是一个重要的前提,主要要求算法简单、内存占用少。网上有许多种方式,一种比较通行的做法是使用5字节的数据,高3位总是“000”,实际使用的低17位二进制。

字段闰月大小标志月份大小标志闰月月份
大小4b12b4b
2017年示例00010101 0001 01110110
描述本年有闰月2,4,8,10,11,12为大月六月是闰月
2019年示例00001010 1001 00110000
描述无闰月1,3,5,8,11,12为大月无闰月

综上所述,2017年信息可以使用 0x15176 表示;2019年信息可使用 0x0a930 表示。

2.2 节气的数据结构

36位字符串

二十四节气开始的日期,与通用的公历几乎一致,最多相差一两天,因为是按照地球一年绕太阳公转一周作为依据。比如小寒通常落在在1月5-7日,立春落在2月3-5日,冬至落在12月21-23日。即每个月都会有2个节气,1月只能有小寒、大寒这两个节气。

构建两个含有24元素的数组,

第一个数组以小寒为第1个节气重新排列这24个节气。

 

  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

结合这两个数组,可记录在一个公历年中,二十四个节气分别是在哪一天。比如上述的24个数字可解释为:1月6日是小寒、1月20日是大寒…12月7日是大雪、12月22日是冬至。

在 Python 语言层面,可以使用字符串(基本数据类型)代替上述数组(复合数据类型),即"620419621520621622723823823924823722" ,需要36位字符存储。

解析表中数据的 Python 代码实现如下:

 

  1. def parse_term(year_info):
  2. result = []
  3. for i in range(0, 36, 3):
  4. s = year_info[i:i + 3]
  5. result.extend([int(s[0]), int(s[1:3])])
  6. return result
'
运行

30位字符串

jjonline/calendar.js 提供了一种用更为简单的表示方法:利用十六进制压缩数字的位数,进一步简化为30位的字符串。具体计算过程如下:

 

  1. 9778397bd097c36b0b6fc9274c91aa # 按长度5分割,共6组
  2. 97783 97bd0 97c36 b0b6f c9274 c91aa # 转化为十进制
  3. 620419 621520 621622 723823 823924 823722 # 按长度1,2,1,2细分
  4. 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代码实现上述算法如下:

 

  1. def parse_term(term_info):
  2. values = [str(int(term_info[i:i + 5], 16)) for i in range(0, 30, 5)]
  3. term_day_list = []
  4. for v in values:
  5. term_day_list.extend([
  6. int(v[0]), int(v[1:3]), int(v[3]), int(v[4:6])
  7. ])
  8. return term_day_list
'
运行

24位字符串

从 Borax v1.2.0 开始使用算法。

统计1900-2100年之前节气日期统计可知,中气的日期都是在18-24日之间,这些均为两位数,可以通过线性变化转为一位数的数字,结合月份特点,可以通过减去一个固定偏移量15就是比较好的选择。

同样的按照上述处理,具体过程如下:

 

  1. 654466556667788888998877 # 按长度1分割
  2. 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
  3. 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位表示法相比,更为简单直接。

 

  1. def parse_term_days(term_info):
  2. return [int(c) + [0, 15][i % 2] for i, c in enumerate(term_info)]
'
运行

3 Borax-Lunardate概述

到2019年1月为止,关于农历的主题,github/PyPI 上已经有非常多的代码项目,语言有C、Java、Python等,具体的思路也不一样。综合来看,这些库有的功能单一,只覆盖某几个方面;有的已经很久没有更新了,主要是农历信息已在多年之前就采集完成,但是对于一些最新的数据修正未能及时涵盖;也有的在代码层面没有很好的适用最新的 Python 语言特性。

基于此,本人利用收集整理的一些技术资料开发出了 Borax-Lunar 这个库,主要的目标和特点有:

  • 完整的农历信息

在开发过程中我收集网络上的几个重要农历数据,包含了干支、生肖、节气等事项,并同时将它们作为数据验证的参考标准。

另外,一些术语命名(比如天干、地支等)采用 《GB/T 33661-2017 农历的编算和颁行》 所规定的文字。

  • 功能完备

Borax-Lunardate 库分为三个部分:1) 基于 LunarDate 的农历日期表示;2)类似于 datetime.strftime 的字符串格式系统;3) 一些常用的农历工具接口。

其中第2,3部分是网络上的农历库比较少涉及的,Borax-Lunardate 在这一方面非常有优势的。

  • 对标datetime

在模块/类层面的组织和分类上,Borax-Lunardate 对标标准库的 datetimecalendar 模块,实现了这两个模块中与农历日期相联系的方法,LunarDatedate 类有许多相同的特性,包括不可变类、可比较性、时间加减等。 甚至有些命名也是一样的,比如 strftime 方法。

 

  1. lunardate.LunarDate <------> datetime.date
  2. lunardate.LCalendars <------> calendar.Calendar

4 模块设计

4.1 LunarDate日期类

4.1.1 初始化日期对象

LunarDate 是一个重要的类,每一个对象表示一个农历日期,一个特定的农历日期可以由农历年、月、日、闰月标志4个字段唯一确定,可以使用这些字段初始化对象。

 

  1. >>>from borax.calendars.lunardate import LunarDate
  2. >>>LunarDate(2018, 7, 1)
  3. LunarDate(2018, 7, 1, 0)

对于一些特定的日期,也可以通过类方法创建这些日期对象。

  1. >>>LunarDate.today()
  2. LunarDate(2018, 7, 1, 0)
  3. >>>LunarDate.yesterday()
  4. LunarDate(2018, 6, 29, 0)
  5. >>>LunarDate.tomorrow()
  6. LunarDate(2018, 7, 2, 0)

4.1.2 基准日期

LunarDate 类使用可表示范围的下限作为基准日期(即LunarDate(1990, 1, 1, False))。对象的 offset 属性表示与基准日期相差的天数,这也是一种可以唯一确定日期的方法。

4.2 格式化显示

该功能由 Borax-Lunardate 特有的功能,提供了与 datetime.date.strftime 相似的功能。

4.2.1 使用方法

LunarDate 提供了 strftime 方法,可以将一个农历日期按照给定的格式转化为字符串。

 

  1. class LunarDate:
  2. def strftime(fmt:str) -> str: pass
'
运行

格式字符串使用 ‘%’ 加一个字母的描述符(Directive)表示日期对象的一个字段,常用的描述符见下表:

属性类型描述示例值格式描述符备注
yearint农历年2018%y 
monthint农历月6%m 
dayint农历日26%d 
leapbool是否闰月False%l(1)
offsetint距下限的偏移量43287- 
termstrNone节气名称立秋%t 
cn_yearstr中文年二〇一八年%Y(2)
cn_monthstr中文月六月%M(2)
cn_daystr中文日廿六%D(2)
gz_yearstr干支年份戊戌%o 
gz_monthstr干支月份庚申%p 
gz_daystr干支日辛未%q 
animalstr年生肖%a 
-str两位数字的月份06%A 
-str两位数字的日期26%B 

备注:

  • (1) ‘%l’ 将闰月标志格式化为数字,如“0”、“1”
  • (2) ‘%Y’、’%M’、’%D’ 三个中文名称不包含“年”、“月”、“日”后缀汉字

下面是几个使用 strftime 的例子:

 

  1. >>>today = LunarDate.today()
  2. >>>today.strftime('%Y-%M-%D')
  3. '二〇一八-六-廿六'
  4. >>>today.strftime('今天的干支表示法为:%G')
  5. '今天的干支表示法为:戊戌年庚申月辛未日'

4.2.2 源码解析

strftime 的具体实现定义在 lunardate.Formatter 类。该类接受一个 %形式的格式字符串,转化为命名字段形式的格式字符串,并格式化给定的日期对象,如下图:

 

'%Y-%M-%D' ==> '{cn_year}-{cn_month}-{cn_day}' ==> '二〇一八-六-廿六'

某个字段 field 的具体值,根据下列先后顺序确定具体的值。

  • Formatter.get_<field>
  • obj.<field>()
  • obj.<field>

核心代码如下:

 

  1. class Formatter:
  2. def resolve(self, obj, field):
  3. try:
  4. func = getattr(self, 'get_' + field)
  5. return func(obj)
  6. except AttributeError:
  7. attr = getattr(obj, field)
  8. if callable(attr):
  9. return attr()
  10. else:
  11. return attr
'
运行

4.3 类型标注

4.3.1 概述

PEP 484PEP526 提供了一种针对 Python 语言的类型标注方法。在 Python3.5+ 以上,可以使用 typing 标准模块实现这一目的,需要说明的是:

  • 方便使用者了解所调用函数的参数类型和返回值类型

  • 类型标注不会影响运行,不会抛出异常,只是警告

  • 配合IDE的语法语义检查功能,增强智能提示功能

    下面是一个参数和返回值都是字符串的函数标注:

    1. def greeting(name: str) -> str:
    2. return 'Hello ' + name
    '
    运行

     

 

除了一些基本类型,常用的符合数据类型还有 AnyUnionTupleListCallableTyVarGeneric 等。

需要注意的是,Python 的语言特性也可能给类型标准的使用带来了一些麻烦,比如变量在使用过程中其类型有所变化。基于目标用户是API使用者,大概可以整理出几条实用的原则:

  • 只应用在公共接口(类、函数、方法、变量)加上类型标注。
  • 全局常量不使用类型标注
  • 魔术方法不使用类型标注
  • 私有方法可以不使用类型标注

4.3.2 常用的使用示例

类型标注学习起来也不困难,掌握几种常见的情形即可。

使用 :表示参数类型,使用 -> 表示返回值类型,如上述的 greeting 函数 。

默认参数

 

def foo(arg: int = 0) -> None: pass'
运行

可选参数,需要使用 Optional,通常和上面的默认参数相互配合。

 

  1. def ndays(year: int, month: Optional[int] = None, leap: bool = False) -> int:
  2. pass

自定义类型、混合类型,闰月标记可以使用布尔值或者整数。

 

Leap = Union[bool, int]

后向引用,如果使用的类还没有定义,可以使用包含类名的字符串,以便后续实例化。一般用于创建对象的方法或者树形结构的定义。

  1. class LunarDate:
  2. def from_solar_date(cls, year: int, month: int, day: int) -> 'LunarDate':
  3. pass
'
运行

类型绑定。在后向引用的例子中,通常需要在多个地方使用字符串方式,为避免拼写错误,可以使用 TypeVar 的 bound 参数提前预定义。

T = TypeVar('T', bound=BaseClass) 使用父类创建类型变量以便所有子类均可匹配,这和 Java/C++ 语言中的 多态 相类似。

 

  1. from typing import TypeVar
  2. T = TypeVar('T', bound='LunarDate')
  3. class LunarDate:
  4. def from_solar_date(cls, year: int, month: int, day: int) -> T:
  5. pass
'
运行

迭代器,使用 Iterator

  1. def hello(n:int) -> Iterator[int]:
  2. for i in range(n):
  3. yield i

5 参考资料

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/1017189
推荐阅读
相关标签
  

闽ICP备14008679号