Borax.Lunardate:中国农历日期
原文地址:https://kinegratii.github.io/2019/01/05/lunardate-module/
感謝原作者!本人只是搬運(yùn)工。看完這個(gè)和上一篇基本對農(nóng)歷就有了一個(gè)較全面的認(rèn)識(shí)。
本文簡要介紹了我國傳統(tǒng)的農(nóng)歷歷法知識(shí),并敘述了 Borax-Lunar 工具庫開發(fā)背后的一些算法原理和技術(shù)資料。
目錄
1 農(nóng)歷概述
1.1 編排規(guī)則
1.2 表示方法
1.3 二十四節(jié)氣
2 數(shù)據(jù)結(jié)構(gòu)
2.1 大小月和閏月
2.2 節(jié)氣的數(shù)據(jù)結(jié)構(gòu)
3 Borax-Lunardate概述
4 模塊設(shè)計(jì)
4.1 LunarDate日期類
4.1.1 初始化日期對象
4.1.2 基準(zhǔn)日期
4.2 格式化顯示
4.2.1 使用方法
4.2.2 源碼解析
4.3 類型標(biāo)注
4.3.1 概述
4.3.2 常用的使用示例
5 參考資料
1 農(nóng)歷概述
農(nóng)歷是我國的傳統(tǒng)歷法,依據(jù)太陽和月球位置的精確預(yù)報(bào)以及約定的日期編排規(guī)則編排日期,并以傳統(tǒng)命名方法表述日期。
2017年,我國已經(jīng)頒布了國家推薦性標(biāo)準(zhǔn)《GB/T 33661-2017 農(nóng)歷的編算和頒行》。
1.1 編排規(guī)則
農(nóng)歷屬于一種陰陽合歷,基本規(guī)則如下:其年份分為平年和閏年。平年為十二個(gè)月;閏年為十三個(gè)月。月份分為大月和小月,大月三十天,小月二十九天。一年中哪個(gè)月大,哪個(gè)月小,可由“置閏規(guī)則”計(jì)算決定。
若從某個(gè)農(nóng)歷十一月開始到下一個(gè)農(nóng)歷十一月(不含)之間有13個(gè)農(nóng)歷月,則需要置閏。置閏規(guī)則為:去其中最先出現(xiàn)的一個(gè)不包含中氣的農(nóng)歷月為農(nóng)歷閏月。
除此之外,還有生肖紀(jì)年、干支紀(jì)年、二十四節(jié)氣等。
1.2 表示方法
農(nóng)歷日期通常有以下幾種表示方法:
- 農(nóng)歷乙未年正月初一
- 農(nóng)歷牛年閏五月十一
- 農(nóng)歷甲午年七月庚戌日
- 公元2016年農(nóng)歷丙申年十一月廿九
1.3 二十四節(jié)氣
一個(gè)回歸年內(nèi)24個(gè)太陽地心視黃經(jīng)等于15度的整數(shù)倍的時(shí)刻的總稱,每個(gè)時(shí)刻成為一個(gè)節(jié)氣。太陽每年運(yùn)行360度,共經(jīng)歷二十四個(gè)節(jié)氣,分別為立春(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度)。可以通過下面的兒歌記憶這些節(jié)氣。
?
春雨驚春清谷天, 夏滿芒夏暑相連, 秋處露秋寒霜降, 冬雪雪冬小大寒, 每月兩節(jié)不變更, 最多相差一兩天2016年11月30日,中國“二十四節(jié)氣”被正式列入聯(lián)合國教科文組織人類非物質(zhì)文化遺產(chǎn)代表作名錄。
2 數(shù)據(jù)結(jié)構(gòu)
農(nóng)歷月份大小、農(nóng)歷閏/平年、二十四節(jié)氣的日期沒有什么特定的規(guī)律,只能使用原始的“查表法”存儲(chǔ)和查詢這些信息。
2.1 大小月和閏月
從香港天文臺(tái)網(wǎng)站可以獲取1900 - 2100年的農(nóng)歷信息,每一天包含公歷日期、農(nóng)歷日期、星期、節(jié)氣四項(xiàng)基本信息。日期范圍的基本信息如下表:
| 公歷 | 1990年1月31日 | … | 2100年12月31日 | 2101年1月1日 | … | 2101年1月28日 |
| 農(nóng)歷 | 1900年正月初一 | … | 2100年十二月初一 | 2100年十二月初二 | … | 2100年十二月二十九 |
| offset | 0 | … | 73383 | 73384 | … | 73411 |
| 干支 | 庚午年丙子月壬辰日 | … | 庚申年戊子月丁未日 | - | … | - |
具體到一個(gè)農(nóng)歷年中,從中可以看出以下幾點(diǎn)信息:
- 每個(gè)月有多少天;哪些是大月(30天),哪些是小月(29天)
- 本年是否有閏月;如果有,是哪個(gè)月份
如何使用精煉的數(shù)據(jù)結(jié)構(gòu)表述這些信息,是一個(gè)重要的前提,主要要求算法簡單、內(nèi)存占用少。網(wǎng)上有許多種方式,一種比較通行的做法是使用5字節(jié)的數(shù)據(jù),高3位總是“000”,實(shí)際使用的低17位二進(jìn)制。
| 大小 | 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 節(jié)氣的數(shù)據(jù)結(jié)構(gòu)
36位字符串
二十四節(jié)氣開始的日期,與通用的公歷幾乎一致,最多相差一兩天,因?yàn)槭前凑盏厍蛞荒昀@太陽公轉(zhuǎn)一周作為依據(jù)。比如小寒通常落在在1月5-7日,立春落在2月3-5日,冬至落在12月21-23日。即每個(gè)月都會(huì)有2個(gè)節(jié)氣,1月只能有小寒、大寒這兩個(gè)節(jié)氣。
構(gòu)建兩個(gè)含有24元素的數(shù)組,
第一個(gè)數(shù)組以小寒為第1個(gè)節(jié)氣重新排列這24個(gè)節(jié)氣。
?
小寒, 大寒, 立春, 雨水, 驚蟄, 春分, 清明, 谷雨, 立夏, 小滿, 芒種, 夏至, 小暑, 大暑, 立秋, 處暑, 白露, 秋分, 寒露, 霜降, 立冬, 小雪, 大雪, 冬至第二個(gè)數(shù)組表示對應(yīng)節(jié)氣對應(yīng)的日期數(shù)字。
?
6 20 4 19 6 21 5 20 6 21 6 22 7 23 8 23 8 23 9 24 8 23 7 22結(jié)合這兩個(gè)數(shù)組,可記錄在一個(gè)公歷年中,二十四個(gè)節(jié)氣分別是在哪一天。比如上述的24個(gè)數(shù)字可解釋為:1月6日是小寒、1月20日是大寒…12月7日是大雪、12月22日是冬至。
在 Python 語言層面,可以使用字符串(基本數(shù)據(jù)類型)代替上述數(shù)組(復(fù)合數(shù)據(jù)類型),即"620419621520621622723823823924823722" ,需要36位字符存儲(chǔ)。
解析表中數(shù)據(jù)的 Python 代碼實(shí)現(xiàn)如下:
?
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 result30位字符串
jjonline/calendar.js 提供了一種用更為簡單的表示方法:利用十六進(jìn)制壓縮數(shù)字的位數(shù),進(jìn)一步簡化為30位的字符串。具體計(jì)算過程如下:
?
9778397bd097c36b0b6fc9274c91aa # 按長度5分割,共6組97783 97bd0 97c36 b0b6f c9274 c91aa # 轉(zhuǎn)化為十進(jìn)制620419 621520 621622 723823 823924 823722 # 按長度1,2,1,2細(xì)分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代碼實(shí)現(xiàn)上述算法如下:
?
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_list24位字符串
從 Borax v1.2.0 開始使用算法。
統(tǒng)計(jì)1900-2100年之前節(jié)氣日期統(tǒng)計(jì)可知,中氣的日期都是在18-24日之間,這些均為兩位數(shù),可以通過線性變化轉(zhuǎn)為一位數(shù)的數(shù)字,結(jié)合月份特點(diǎn),可以通過減去一個(gè)固定偏移量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,偶位置為156 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 Borax-Lunardate概述
到2019年1月為止,關(guān)于農(nóng)歷的主題,github/PyPI 上已經(jīng)有非常多的代碼項(xiàng)目,語言有C、Java、Python等,具體的思路也不一樣。綜合來看,這些庫有的功能單一,只覆蓋某幾個(gè)方面;有的已經(jīng)很久沒有更新了,主要是農(nóng)歷信息已在多年之前就采集完成,但是對于一些最新的數(shù)據(jù)修正未能及時(shí)涵蓋;也有的在代碼層面沒有很好的適用最新的 Python 語言特性。
基于此,本人利用收集整理的一些技術(shù)資料開發(fā)出了 Borax-Lunar 這個(gè)庫,主要的目標(biāo)和特點(diǎn)有:
- 完整的農(nóng)歷信息
在開發(fā)過程中我收集網(wǎng)絡(luò)上的幾個(gè)重要農(nóng)歷數(shù)據(jù),包含了干支、生肖、節(jié)氣等事項(xiàng),并同時(shí)將它們作為數(shù)據(jù)驗(yàn)證的參考標(biāo)準(zhǔn)。
另外,一些術(shù)語命名(比如天干、地支等)采用 《GB/T 33661-2017 農(nóng)歷的編算和頒行》 所規(guī)定的文字。
- 功能完備
Borax-Lunardate 庫分為三個(gè)部分:1) 基于 LunarDate 的農(nóng)歷日期表示;2)類似于 datetime.strftime 的字符串格式系統(tǒng);3) 一些常用的農(nóng)歷工具接口。
其中第2,3部分是網(wǎng)絡(luò)上的農(nóng)歷庫比較少涉及的,Borax-Lunardate 在這一方面非常有優(yōu)勢的。
- 對標(biāo)datetime
在模塊/類層面的組織和分類上,Borax-Lunardate 對標(biāo)標(biāo)準(zhǔn)庫的 datetime 和 calendar 模塊,實(shí)現(xiàn)了這兩個(gè)模塊中與農(nóng)歷日期相聯(lián)系的方法,LunarDate 和 date 類有許多相同的特性,包括不可變類、可比較性、時(shí)間加減等。 甚至有些命名也是一樣的,比如 strftime 方法。
?
lunardate.LunarDate <------> datetime.datelunardate.LCalendars <------> calendar.Calendar4 模塊設(shè)計(jì)
4.1 LunarDate日期類
4.1.1 初始化日期對象
LunarDate 是一個(gè)重要的類,每一個(gè)對象表示一個(gè)農(nóng)歷日期,一個(gè)特定的農(nóng)歷日期可以由農(nóng)歷年、月、日、閏月標(biāo)志4個(gè)字段唯一確定,可以使用這些字段初始化對象。
?
>>>from borax.calendars.lunardate import LunarDate >>>LunarDate(2018, 7, 1) LunarDate(2018, 7, 1, 0)對于一些特定的日期,也可以通過類方法創(chuàng)建這些日期對象。
>>>LunarDate.today() LunarDate(2018, 7, 1, 0)>>>LunarDate.yesterday() LunarDate(2018, 6, 29, 0)>>>LunarDate.tomorrow() LunarDate(2018, 7, 2, 0)4.1.2 基準(zhǔn)日期
LunarDate 類使用可表示范圍的下限作為基準(zhǔn)日期(即LunarDate(1990, 1, 1, False))。對象的 offset 屬性表示與基準(zhǔn)日期相差的天數(shù),這也是一種可以唯一確定日期的方法。
4.2 格式化顯示
該功能由 Borax-Lunardate 特有的功能,提供了與 datetime.date.strftime 相似的功能。
4.2.1 使用方法
LunarDate 提供了 strftime 方法,可以將一個(gè)農(nóng)歷日期按照給定的格式轉(zhuǎn)化為字符串。
?
class LunarDate:def strftime(fmt:str) -> str: pass格式字符串使用 ‘%’ 加一個(gè)字母的描述符(Directive)表示日期對象的一個(gè)字段,常用的描述符見下表:
| year | int | 農(nóng)歷年 | 2018 | %y | ? |
| month | int | 農(nóng)歷月 | 6 | %m | ? |
| day | int | 農(nóng)歷日 | 26 | %d | ? |
| leap | bool | 是否閏月 | False | %l | (1) |
| offset | int | 距下限的偏移量 | 43287 | - | ? |
| term | str 或 None | 節(jié)氣名稱 | 立秋 | %t | ? |
| cn_year | str | 中文年 | 二〇一八年 | %Y | (2) |
| cn_month | str | 中文月 | 六月 | %M | (2) |
| cn_day | str | 中文日 | 廿六 | %D | (2) |
| gz_year | str | 干支年份 | 戊戌 | %o | ? |
| gz_month | str | 干支月份 | 庚申 | %p | ? |
| gz_day | str | 干支日 | 辛未 | %q | ? |
| animal | str | 年生肖 | 狗 | %a | ? |
| - | str | 兩位數(shù)字的月份 | 06 | %A | ? |
| - | str | 兩位數(shù)字的日期 | 26 | %B | ? |
備注:
- (1) ‘%l’ 將閏月標(biāo)志格式化為數(shù)字,如“0”、“1”
- (2) ‘%Y’、’%M’、’%D’ 三個(gè)中文名稱不包含“年”、“月”、“日”后綴漢字
下面是幾個(gè)使用 strftime 的例子:
?
>>>today = LunarDate.today() >>>today.strftime('%Y-%M-%D') '二〇一八-六-廿六' >>>today.strftime('今天的干支表示法為:%G') '今天的干支表示法為:戊戌年庚申月辛未日'4.2.2 源碼解析
strftime 的具體實(shí)現(xiàn)定義在 lunardate.Formatter 類。該類接受一個(gè) %形式的格式字符串,轉(zhuǎn)化為命名字段形式的格式字符串,并格式化給定的日期對象,如下圖:
?
'%Y-%M-%D' ==> '{cn_year}-{cn_month}-{cn_day}' ==> '二〇一八-六-廿六'某個(gè)字段 field 的具體值,根據(jù)下列先后順序確定具體的值。
- Formatter.get_<field>
- obj.<field>()
- obj.<field>
核心代碼如下:
?
class Formatter:def resolve(self, obj, field):try:func = getattr(self, 'get_' + field)return func(obj)except AttributeError:attr = getattr(obj, field)if callable(attr):return attr()else:return attr4.3 類型標(biāo)注
4.3.1 概述
PEP 484 和 PEP526 提供了一種針對 Python 語言的類型標(biāo)注方法。在 Python3.5+ 以上,可以使用 typing 標(biāo)準(zhǔn)模塊實(shí)現(xiàn)這一目的,需要說明的是:
-
方便使用者了解所調(diào)用函數(shù)的參數(shù)類型和返回值類型
-
類型標(biāo)注不會(huì)影響運(yùn)行,不會(huì)拋出異常,只是警告
-
配合IDE的語法語義檢查功能,增強(qiáng)智能提示功能
下面是一個(gè)參數(shù)和返回值都是字符串的函數(shù)標(biāo)注:
def greeting(name: str) -> str:return 'Hello ' + name?
| ? |
除了一些基本類型,常用的符合數(shù)據(jù)類型還有 Any、Union、Tuple、List、Callable、TyVar、Generic 等。
需要注意的是,Python 的語言特性也可能給類型標(biāo)準(zhǔn)的使用帶來了一些麻煩,比如變量在使用過程中其類型有所變化。基于目標(biāo)用戶是API使用者,大概可以整理出幾條實(shí)用的原則:
- 只應(yīng)用在公共接口(類、函數(shù)、方法、變量)加上類型標(biāo)注。
- 全局常量不使用類型標(biāo)注
- 魔術(shù)方法不使用類型標(biāo)注
- 私有方法可以不使用類型標(biāo)注
4.3.2 常用的使用示例
類型標(biāo)注學(xué)習(xí)起來也不困難,掌握幾種常見的情形即可。
使用 :表示參數(shù)類型,使用 -> 表示返回值類型,如上述的 greeting 函數(shù) 。
默認(rèn)參數(shù)
?
def foo(arg: int = 0) -> None: pass可選參數(shù),需要使用 Optional,通常和上面的默認(rèn)參數(shù)相互配合。
?
def ndays(year: int, month: Optional[int] = None, leap: bool = False) -> int:pass自定義類型、混合類型,閏月標(biāo)記可以使用布爾值或者整數(shù)。
?
Leap = Union[bool, int]后向引用,如果使用的類還沒有定義,可以使用包含類名的字符串,以便后續(xù)實(shí)例化。一般用于創(chuàng)建對象的方法或者樹形結(jié)構(gòu)的定義。
class LunarDate:def from_solar_date(cls, year: int, month: int, day: int) -> 'LunarDate':pass類型綁定。在后向引用的例子中,通常需要在多個(gè)地方使用字符串方式,為避免拼寫錯(cuò)誤,可以使用 TypeVar 的 bound 參數(shù)提前預(yù)定義。
T = TypeVar('T', bound=BaseClass) 使用父類創(chuàng)建類型變量以便所有子類均可匹配,這和 Java/C++ 語言中的 多態(tài) 相類似。
?
from typing import TypeVarT = TypeVar('T', bound='LunarDate')class LunarDate:def from_solar_date(cls, year: int, month: int, day: int) -> T:pass迭代器,使用 Iterator。
def hello(n:int) -> Iterator[int]:for i in range(n):yield i5 參考資料
- 香港天文臺(tái)農(nóng)歷信息
- “農(nóng)歷”維基詞條
- jjonline/calendar.js
- lidaobing/python-lunardate
- Forward references - Stackflow
?
總結(jié)
以上是生活随笔為你收集整理的Borax.Lunardate:中国农历日期的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安全普及:关于网络远程控制和木马的几点误
- 下一篇: 求助!网站重构需要帮手(前端)