"""solar_terms.py — sxtwl 기반 24절기 + 月支 매핑 테스트. Reference fixture(`reference_saju.json`)는 saju-web의 solarlunar gzMonth를 직접 사용해 재생성되었다 (이전 버전은 `getCurrentSolarTerm` 루프 버그로 모든 케이스가 月支='丑'으로 떨어졌음). 본 테스트는 - 절기 boundary (입춘 day) 동작 검증 - sxtwl JieQi index 순서 가정 검증 - 月支 자체 일관성(여러 날짜에서 기대되는 月支 산출) 검증 - Reference fixture 30 케이스 sxtwl ↔ solarlunar 일치 검증 네 가지로 구성한다. """ import json from pathlib import Path import pytest from app.calculator import solar_terms as st REF_PATH = Path(__file__).parent / "fixtures" / "reference_saju.json" REF = json.loads(REF_PATH.read_text(encoding="utf-8")) BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"] def test_jieqi_index_order_lichun_is_3(): """sxtwl JieQi index에서 立春은 3(冬至=0 기준 황경 +45°). 1995-02-04는 입춘 당일. sxtwl이 그 날짜에 jqIndex=3을 반환해야 함. """ import sxtwl qi_list = sxtwl.getJieQiByYear(1995) # 1995-02-04 ±1일 안에 jqIndex=3인 항목이 있어야 함 found = False for info in qi_list: t = sxtwl.JD2DD(info.jd) if int(info.jqIndex) == 3 and int(t.Y) == 1995 and int(t.M) == 2 and 3 <= int(t.D) <= 5: found = True break assert found, "1995년 立春(jqIndex=3) 절기가 2/3~2/5 사이에서 발견되지 않음" def test_get_current_solar_term_ipchun_boundary(): """1995-02-04는 입춘 당일.""" idx_4 = st.get_current_solar_term(1995, 2, 4) idx_5 = st.get_current_solar_term(1995, 2, 5) idx_3 = st.get_current_solar_term(1995, 2, 3) # 2/4와 2/5는 같은 절기 인덱스 (立春=3) assert idx_4 == idx_5 # 2/3은 다른 인덱스 (立春 이전 — 大寒=2) assert idx_4 != idx_3 def test_get_solar_term_month_branch_ipchun_day(): """1995-02-04는 입춘 → 月支 = 寅(인덱스 2).""" branch_idx = st.get_solar_term_month_branch(1995, 2, 4) assert BRANCHES[branch_idx] == "寅" def test_get_solar_term_month_branch_before_ipchun(): """1995-02-03은 입춘 이전 → 月支 = 丑(인덱스 1).""" branch_idx = st.get_solar_term_month_branch(1995, 2, 3) assert BRANCHES[branch_idx] == "丑" @pytest.mark.parametrize( "year,month,day,expected_branch", [ # 표준 절기 경계 기준 月支 검증 (saju 일반 상식) (1990, 5, 15, "巳"), # 立夏(5/6) ~ 芒種(6/6) 사이 → 巳월 (1990, 6, 6, "午"), # 芒種 당일 → 午월 (1980, 6, 6, "午"), # 芒種 ~ 小暑 → 午월 (1995, 2, 5, "寅"), # 立春 다음날 → 寅 (1995, 2, 3, "丑"), # 立春 이전 → 丑 (2000, 2, 29, "寅"), # 立春 이후 → 寅 (2010, 12, 31, "子"), # 大雪 이후 → 子 (1985, 1, 1, "子"), # 1/1은 小寒(1/5) 이전 → 子 (1985, 1, 20, "丑"), # 小寒 이후 → 丑 (1972, 7, 24, "未"), # 小暑 ~ 立秋 사이 → 未 (1992, 8, 8, "申"), # 立秋 ~ 白露 → 申 (1988, 9, 9, "酉"), # 白露 ~ 寒露 → 酉 (2005, 6, 22, "午"), # 芒種 ~ 小暑 → 午 (2020, 9, 23, "酉"), # 白露 ~ 寒露 → 酉 ], ) def test_month_branch_standard_cases(year, month, day, expected_branch): """일반 사주 상식 기준 月支 검증 — sxtwl 절기와 일치해야 함.""" branch_idx = st.get_solar_term_month_branch(year, month, day) actual = BRANCHES[branch_idx] assert actual == expected_branch, ( f"{year}-{month:02d}-{day:02d}: expected {expected_branch}, got {actual}" ) def test_get_days_to_next_solar_term_positive(): """다음 절기까지 일수는 항상 양수, 16일 이내.""" # 立春(2/4) 다음날 → 雨水(2/19)까지 약 15일 days = st.get_days_to_next_solar_term(1995, 2, 5) assert 1 <= days <= 16, f"days = {days} (expected 1~16)" @pytest.mark.parametrize( "case", REF, ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}", ) def test_month_branch_matches_reference(case): """Reference fixture의 month branch(solarlunar gzMonth 기준)와 sxtwl 계산 일치.""" inp = case["input"] expected_branch = case["expected"]["saju"]["month"]["branch"] branch_idx = st.get_solar_term_month_branch(inp["year"], inp["month"], inp["day"]) actual_branch = BRANCHES[branch_idx] assert actual_branch == expected_branch, ( f"mismatch for {inp}: got {actual_branch}, expected {expected_branch}" )