This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user