"""calculator/core.py — saju-web TS 엔진과 1:1 매칭 검증. fixtures/reference_saju.json 의 30 case (TS calculateSaju 결과)와 Python 구현 일치 여부 검증. """ import json from pathlib import Path import pytest from app.calculator.core import calculate_saju REF_PATH = Path(__file__).parent / "fixtures" / "reference_saju.json" REF = json.loads(REF_PATH.read_text(encoding="utf-8")) def _camel_to_snake(name: str) -> str: out = [] for ch in name: if ch.isupper(): out.append("_" + ch.lower()) else: out.append(ch) return "".join(out) def _normalize(d): """TS camelCase → Python snake_case 변환 (deep).""" if isinstance(d, dict): return {_camel_to_snake(k): _normalize(v) for k, v in d.items()} if isinstance(d, list): return [_normalize(x) for x in d] return d @pytest.mark.parametrize( "case", REF, ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}-{c['input'].get('hour')}-{c['input']['gender']}", ) def test_calculate_saju_matches_reference(case): inp = case["input"] expected = _normalize(case["expected"]["saju"]) actual = calculate_saju( inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"], ) # 4기둥 비교 for pillar in ["year", "month", "day", "hour"]: if expected.get(pillar) is None: assert actual.get(pillar) is None, f"{pillar} should be None" continue if pillar == "hour" and inp.get("hour") is None: assert actual.get(pillar) is None continue ep = expected[pillar] ap = actual[pillar] for field in ["stem", "branch", "stem_kr", "branch_kr", "element", "ten_god", "fortune"]: assert ap[field] == ep[field], ( f"{pillar}.{field}: actual={ap[field]} expected={ep[field]} " f"(input={inp})" ) # day_stem assert actual["day_stem"] == expected["day_stem"] # gender assert actual["gender"] == expected["gender"] # birth_date bd = actual["birth_date"] assert bd["year"] == inp["year"] assert bd["month"] == inp["month"] assert bd["day"] == inp["day"] if inp.get("hour") is not None: assert bd.get("hour") == inp["hour"]