初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,216 @@
|
||||
"""农历/节气/节日/时辰引擎。
|
||||
|
||||
依赖 ``lunar_python``(寿星天文历 Python 版)做历法换算与节气判定;节日按
|
||||
公历固定日期 + 农历日期两张表命中。全部纯函数,无副作用。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from lunar_python import Solar
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class LunarDate(BaseModel):
|
||||
"""农历日期。"""
|
||||
|
||||
year_ganzhi: str
|
||||
month: int
|
||||
day: int
|
||||
is_leap_month: bool
|
||||
|
||||
|
||||
class SolarTermInfo(BaseModel):
|
||||
"""节气信息。"""
|
||||
|
||||
current_name: str | None
|
||||
next_name: str
|
||||
days_to_next: int
|
||||
|
||||
|
||||
class Festival(BaseModel):
|
||||
"""节日命中。"""
|
||||
|
||||
name: str
|
||||
calendar: str # solar / lunar
|
||||
locale: str = "zh-CN"
|
||||
|
||||
|
||||
class Shichen(BaseModel):
|
||||
"""十二时辰。"""
|
||||
|
||||
name: str
|
||||
start_hour: int
|
||||
end_hour: int
|
||||
|
||||
|
||||
# 十二时辰:name -> (start, end),跨零点的时辰 end < start(子时 23->1)
|
||||
_SHICHEN_TABLE: list[tuple[str, int, int]] = [
|
||||
("子", 23, 1),
|
||||
("丑", 1, 3),
|
||||
("寅", 3, 5),
|
||||
("卯", 5, 7),
|
||||
("辰", 7, 9),
|
||||
("巳", 9, 11),
|
||||
("午", 11, 13),
|
||||
("未", 13, 15),
|
||||
("申", 15, 17),
|
||||
("酉", 17, 19),
|
||||
("戌", 19, 21),
|
||||
("亥", 21, 23),
|
||||
]
|
||||
|
||||
# 公历固定节日(month, day) -> name
|
||||
_SOLAR_FESTIVALS: dict[tuple[int, int], str] = {
|
||||
(1, 1): "元旦",
|
||||
(2, 14): "情人节",
|
||||
(5, 1): "劳动节",
|
||||
(10, 1): "国庆节",
|
||||
(12, 25): "圣诞节",
|
||||
}
|
||||
|
||||
# lunar_python 的 getJieQiTable() 对跨年带入的节气用大写拼音常量作 key
|
||||
# (实证于 lunar_python 1.4.8:DA_HAN/DONG_ZHI/JING_ZHE/LI_CHUN/XIAO_HAN/YU_SHUI 等),
|
||||
# 本表内节气用中文 key。此处把 24 节气拼音常量统一翻成中文,已是中文的保持不变。
|
||||
_JIEQI_PINYIN_TO_CN: dict[str, str] = {
|
||||
"LI_CHUN": "立春",
|
||||
"YU_SHUI": "雨水",
|
||||
"JING_ZHE": "惊蛰",
|
||||
"CHUN_FEN": "春分",
|
||||
"QING_MING": "清明",
|
||||
"GU_YU": "谷雨",
|
||||
"LI_XIA": "立夏",
|
||||
"XIAO_MAN": "小满",
|
||||
"MANG_ZHONG": "芒种",
|
||||
"XIA_ZHI": "夏至",
|
||||
"XIAO_SHU": "小暑",
|
||||
"DA_SHU": "大暑",
|
||||
"LI_QIU": "立秋",
|
||||
"CHU_SHU": "处暑",
|
||||
"BAI_LU": "白露",
|
||||
"QIU_FEN": "秋分",
|
||||
"HAN_LU": "寒露",
|
||||
"SHUANG_JIANG": "霜降",
|
||||
"LI_DONG": "立冬",
|
||||
"XIAO_XUE": "小雪",
|
||||
"DA_XUE": "大雪",
|
||||
"DONG_ZHI": "冬至",
|
||||
"XIAO_HAN": "小寒",
|
||||
"DA_HAN": "大寒",
|
||||
}
|
||||
|
||||
|
||||
# 农历节日(lunar_month, lunar_day, is_leap) -> name
|
||||
_LUNAR_FESTIVALS: dict[tuple[int, int, bool], str] = {
|
||||
(1, 1, False): "春节",
|
||||
(1, 15, False): "元宵节",
|
||||
(5, 5, False): "端午节",
|
||||
(7, 7, False): "七夕",
|
||||
(8, 15, False): "中秋节",
|
||||
(9, 9, False): "重阳节",
|
||||
(12, 8, False): "腊八节",
|
||||
}
|
||||
|
||||
|
||||
def solar_to_lunar(solar_date: date) -> LunarDate:
|
||||
"""公历日期转农历日期。
|
||||
|
||||
``lunar_python`` 的 ``getMonth()`` 对闰月返回负数(如 -6 表示闰六月),
|
||||
非闰月返回正数;据此推导 ``is_leap_month`` 与展示用的月份数值。
|
||||
"""
|
||||
solar = Solar.fromYmd(solar_date.year, solar_date.month, solar_date.day)
|
||||
lunar = solar.getLunar()
|
||||
raw_month = lunar.getMonth()
|
||||
return LunarDate(
|
||||
year_ganzhi=lunar.getYearInGanZhi(),
|
||||
month=abs(raw_month),
|
||||
day=int(lunar.getDay()),
|
||||
is_leap_month=raw_month < 0,
|
||||
)
|
||||
|
||||
|
||||
def get_shichen(hour: int) -> Shichen:
|
||||
"""按 24 小时制小时返回十二时辰。子时跨零点(23 与 0 均属子)。"""
|
||||
h = hour % 24
|
||||
for name, start, end in _SHICHEN_TABLE:
|
||||
if start < end:
|
||||
if start <= h < end:
|
||||
return Shichen(name=name, start_hour=start, end_hour=end)
|
||||
else: # 跨零点(子时 23->1)
|
||||
if h >= start or h < end:
|
||||
return Shichen(name=name, start_hour=start, end_hour=end)
|
||||
# 理论不可达(24 小时全覆盖)
|
||||
raise ValueError(f"无法识别时辰:hour={hour}")
|
||||
|
||||
|
||||
def get_day_period(hour: int) -> str:
|
||||
"""返回现代时段枚举值(契约字典 day_period)。"""
|
||||
h = hour % 24
|
||||
if 0 <= h < 5:
|
||||
return "凌晨"
|
||||
if 5 <= h < 8:
|
||||
return "清晨"
|
||||
if 8 <= h < 11:
|
||||
return "上午"
|
||||
if 11 <= h < 13:
|
||||
return "中午"
|
||||
if 13 <= h < 17:
|
||||
return "下午"
|
||||
if 17 <= h < 19:
|
||||
return "傍晚"
|
||||
if 19 <= h < 23:
|
||||
return "夜晚"
|
||||
return "深夜" # 23-24
|
||||
|
||||
|
||||
def get_solar_term(solar_date: date) -> SolarTermInfo:
|
||||
"""返回当日命中的节气与下一节气信息。
|
||||
|
||||
``lunar_python`` 的 ``getJieQiTable()`` 返回 dict(节气名 -> ``Solar``);
|
||||
值本身即 ``Solar`` 时刻对象,无需再调 ``getSolar()``。据此判定当日是否
|
||||
命中某节气,并算出距下一节气的天数。
|
||||
"""
|
||||
solar = Solar.fromYmd(solar_date.year, solar_date.month, solar_date.day)
|
||||
lunar = solar.getLunar()
|
||||
jieqi = lunar.getJieQiTable() # dict: 节气名 -> Solar 时刻
|
||||
|
||||
current_name: str | None = None
|
||||
for raw_name, moment in jieqi.items():
|
||||
if (
|
||||
moment.getYear() == solar_date.year
|
||||
and moment.getMonth() == solar_date.month
|
||||
and moment.getDay() == solar_date.day
|
||||
):
|
||||
current_name = _JIEQI_PINYIN_TO_CN.get(raw_name, raw_name)
|
||||
break
|
||||
|
||||
# 下一节气:从所有节气时刻中找当前日期之后最近的
|
||||
upcoming: list[tuple[int, str, date]] = []
|
||||
for raw_name, moment in jieqi.items():
|
||||
d = date(moment.getYear(), moment.getMonth(), moment.getDay())
|
||||
if d > solar_date:
|
||||
upcoming.append(((d - solar_date).days, _JIEQI_PINYIN_TO_CN.get(raw_name, raw_name), d))
|
||||
upcoming.sort()
|
||||
days_to_next, next_name, _ = upcoming[0]
|
||||
return SolarTermInfo(current_name=current_name, next_name=next_name, days_to_next=days_to_next)
|
||||
|
||||
|
||||
def get_festivals(solar_date: date, locale: str = "zh-CN") -> list[Festival]:
|
||||
"""命中今日的公历与农历节日。"""
|
||||
result: list[Festival] = []
|
||||
# 公历
|
||||
if (solar_date.month, solar_date.day) in _SOLAR_FESTIVALS:
|
||||
result.append(
|
||||
Festival(
|
||||
name=_SOLAR_FESTIVALS[(solar_date.month, solar_date.day)],
|
||||
calendar="solar",
|
||||
locale=locale,
|
||||
)
|
||||
)
|
||||
# 农历
|
||||
lunar = solar_to_lunar(solar_date)
|
||||
key = (lunar.month, lunar.day, lunar.is_leap_month)
|
||||
if key in _LUNAR_FESTIVALS:
|
||||
result.append(Festival(name=_LUNAR_FESTIVALS[key], calendar="lunar", locale=locale))
|
||||
return result
|
||||
Reference in new issue
Block a user