60 lines
2.1 KiB
Python
60 lines
2.1 KiB
Python
"""Pure lunar-calendar conversion helpers for countdowns.
|
|
|
|
Lunar months use lunar-python's convention: 1..12 are regular months and
|
|
-1..-12 are leap months. A missing leap month or invalid lunar day returns None.
|
|
"""
|
|
|
|
from datetime import date
|
|
|
|
from lunar_python import Lunar, Solar
|
|
|
|
|
|
def lunar_to_solar_safe(year: int, month: int, day: int) -> date | None:
|
|
"""Convert a lunar date without allowing library exceptions to escape."""
|
|
try:
|
|
solar = Lunar.fromYmd(year, month, day).getSolar()
|
|
result = date(solar.getYear(), solar.getMonth(), solar.getDay())
|
|
lunar = solar.getLunar()
|
|
if (lunar.getYear(), lunar.getMonth(), lunar.getDay()) != (year, month, day):
|
|
return None
|
|
return result
|
|
except Exception: # noqa: BLE001 - library raises plain Exception for invalid leap months.
|
|
return None
|
|
|
|
|
|
def _lunar_date(value: date):
|
|
return Solar.fromYmd(value.year, value.month, value.day).getLunar()
|
|
|
|
|
|
def solar_to_lunar_parts(value: date) -> tuple[int, int, int]:
|
|
lunar = _lunar_date(value)
|
|
return lunar.getYear(), lunar.getMonth(), lunar.getDay()
|
|
|
|
|
|
def solar_to_lunar_text(value: date) -> str:
|
|
lunar = _lunar_date(value)
|
|
return f"农历{lunar.getMonthInChinese()}月{lunar.getDayInChinese()}"
|
|
|
|
|
|
def lunar_label_with_year(value: date) -> str:
|
|
lunar = _lunar_date(value)
|
|
return f"农历{lunar.getYearInChinese()}年{lunar.getMonthInChinese()}月{lunar.getDayInChinese()}"
|
|
|
|
|
|
def next_lunar_occurrence(
|
|
month: int,
|
|
day: int,
|
|
ignore_year: bool,
|
|
repeat_rule: str,
|
|
today: date,
|
|
) -> date | None:
|
|
"""Find the next matching lunar month/day, including sparse leap months."""
|
|
del ignore_year, repeat_rule # Both make lunar month/day recur by lunar year.
|
|
# Leap months can be separated by more than a decade. The library supports a
|
|
# bounded year range, so search far enough for all practical countdowns.
|
|
for year in range(today.year - 1, today.year + 101):
|
|
candidate = lunar_to_solar_safe(year, month, day)
|
|
if candidate is not None and candidate >= today:
|
|
return candidate
|
|
return None
|