"""24절기 + sxtwl 기반 月支 계산. sxtwl(寿星天文历) 2.x의 JieQi index 순서: 0=冬至, 1=小寒, 2=大寒, 3=立春, 4=雨水, 5=驚蟄, 6=春分, 7=清明, 8=穀雨, 9=立夏, 10=小滿, 11=芒種, 12=夏至, 13=小暑, 14=大暑, 15=立秋, 16=處暑, 17=白露, 18=秋分, 19=寒露, 20=霜降, 21=立冬, 22=小雪, 23=大雪 (冬至부터 15° 간격으로 황경 증가 — sxtwl.cpp 의 `xn % 24` 계산식 기준) saju에서 月支는 12개의 '절'(節, 홀수 황경 인덱스에 해당)을 기준으로 12구간으로 나눈다: 立春(3) → 寅(2) 驚蟄(5) → 卯(3) 清明(7) → 辰(4) 立夏(9) → 巳(5) 芒種(11) → 午(6) 小暑(13) → 未(7) 立秋(15) → 申(8) 白露(17) → 酉(9) 寒露(19) → 戌(10) 立冬(21) → 亥(11) 大雪(23) → 子(0) 小寒(1) → 丑(1) """ from __future__ import annotations from datetime import date as _date from functools import lru_cache import sxtwl # sxtwl JieQi index → 月支 인덱스 (子=0, 丑=1, 寅=2, ..., 亥=11) # 12 '節' 만 매핑 (홀수 인덱스), '氣' (짝수 인덱스)는 月支 경계가 아님 → 직전 절의 月支 유지 JIEQI_TO_BRANCH: dict[int, int] = { 3: 2, # 立春 → 寅 5: 3, # 驚蟄 → 卯 7: 4, # 清明 → 辰 9: 5, # 立夏 → 巳 11: 6, # 芒種 → 午 13: 7, # 小暑 → 未 15: 8, # 立秋 → 申 17: 9, # 白露 → 酉 19: 10, # 寒露 → 戌 21: 11, # 立冬 → 亥 23: 0, # 大雪 → 子 1: 1, # 小寒 → 丑 } @lru_cache(maxsize=64) def _all_jieqi_dates(year: int) -> tuple[tuple[_date, int], ...]: """주어진 연도의 24절기 (date, jieqi_index) 튜플 리스트. sxtwl.getJieQiByYear(year) → list[JieQiInfo(jd, jqIndex)] 사용. sxtwl.JD2DD(jd) → Time(Y, M, D, ...)로 양력 변환. @lru_cache로 같은 연도 반복 호출 시 sxtwl 재호출 방지. """ result: list[tuple[_date, int]] = [] jq_list = sxtwl.getJieQiByYear(year) for info in jq_list: t = sxtwl.JD2DD(info.jd) try: d = _date(int(t.Y), int(t.M), int(t.D)) except (ValueError, OverflowError): continue result.append((d, int(info.jqIndex))) return tuple(result) def get_current_solar_term(year: int, month: int, day: int) -> int: """현재 날짜에 적용되는 절기 인덱스 (0~23, sxtwl 기준) 반환. 입력 날짜 이전(또는 같은) 가장 가까운 절기의 인덱스를 반환. 절기가 아직 들어오지 않은 경우(연초 등) ±1년치를 함께 탐색. """ target = _date(year, month, day) candidates: list[tuple[_date, int]] = [] for y in (year - 1, year, year + 1): candidates.extend(_all_jieqi_dates(y)) candidates.sort() last_idx = 0 # 폴백 for d, idx in candidates: if d <= target: last_idx = idx else: break return last_idx def get_solar_term_month_branch(year: int, month: int, day: int) -> int: """현재 날짜의 月支 인덱스 (0~11, 子=0...亥=11). 절(節) 기준 12구간. 12개 '절'만 추출해, 입력 날짜 이전 가장 가까운 절의 月支를 반환. 立春 이전이면 직전 해 小寒(=丑) 또는 大雪(=子) 기준. """ target = _date(year, month, day) jie_dates: list[tuple[_date, int]] = [] for y in (year - 1, year, year + 1): for d, qi_idx in _all_jieqi_dates(y): if qi_idx in JIEQI_TO_BRANCH: jie_dates.append((d, JIEQI_TO_BRANCH[qi_idx])) jie_dates.sort() last_branch = 1 # 立春 이전 폴백 — 丑(축) for d, branch_idx in jie_dates: if d <= target: last_branch = branch_idx else: break return last_branch def get_days_to_next_solar_term(year: int, month: int, day: int) -> int: """다음 절기까지 남은 일수 (대운 계산용). 24절기 모두 대상. 입력 날짜 이후 가장 가까운 절기까지의 일 수 반환. """ target = _date(year, month, day) all_qi: list[tuple[_date, int]] = [] for y in (year, year + 1): all_qi.extend(_all_jieqi_dates(y)) all_qi.sort() for d, _idx in all_qi: if d > target: return (d - target).days return 30 # 폴백 (이론상 도달 불가)