初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+216
View File
@@ -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