feat(saju-lab): daeun.py — 대운 8개 계산 (30/30 reference)

This commit is contained in:
2026-05-25 20:09:46 +09:00
parent ebfade655a
commit db1f69c7a5
2 changed files with 384 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
"""daeun.py — 대운 8개 계산 검증.
fixtures/reference_saju.json 의 30 case (TS) 와 Python 구현 1:1 일치 검증.
"""
import json
from pathlib import Path
import pytest
from app.calculator.core import calculate_saju
from app.calculator.daeun import calculate_daeun, get_current_daeun, get_daeun_description
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']['gender']}",
)
def test_calculate_daeun_matches_reference(case):
inp = case["input"]
expected = [_normalize(x) for x in case["expected"]["daeun"]]
saju = calculate_saju(
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
)
actual = calculate_daeun(
inp["year"],
inp["month"],
inp["day"],
inp["gender"],
saju["month"]["stem"],
saju["month"]["branch"],
)
assert len(actual) == len(expected), (
f"len mismatch for {inp}: {len(actual)} vs {len(expected)}"
)
for i, (a, e) in enumerate(zip(actual, expected)):
for key in ("stem", "branch", "stem_kr", "branch_kr"):
assert a.get(key) == e.get(key), (
f"{inp} daeun[{i}].{key}: actual={a.get(key)} expected={e.get(key)}"
)
# age + start_year/end_year
assert a.get("age") == e.get("age"), (
f"{inp} daeun[{i}].age: {a.get('age')} vs {e.get('age')}"
)
assert a.get("start_year") == e.get("start_year"), (
f"{inp} daeun[{i}].start_year: {a.get('start_year')} vs {e.get('start_year')}"
)
assert a.get("end_year") == e.get("end_year"), (
f"{inp} daeun[{i}].end_year: {a.get('end_year')} vs {e.get('end_year')}"
)
def test_get_current_daeun_returns_match():
daeun_list = [
{"age": 10, "start_year": 2000, "end_year": 2009},
{"age": 20, "start_year": 2010, "end_year": 2019},
{"age": 30, "start_year": 2020, "end_year": 2029},
]
assert get_current_daeun(daeun_list, 2005) == daeun_list[0]
assert get_current_daeun(daeun_list, 2015) == daeun_list[1]
assert get_current_daeun(daeun_list, 2025) == daeun_list[2]
assert get_current_daeun(daeun_list, 1999) is None
assert get_current_daeun(daeun_list, 2030) is None
def test_get_daeun_description_returns_string():
daeun = {
"age": 10,
"start_year": 2000,
"end_year": 2009,
"stem": "",
"branch": "",
"stem_kr": "",
"branch_kr": "",
}
desc = get_daeun_description(daeun, "")
assert isinstance(desc, str)
assert "임오" in desc
assert "壬午" in desc
assert "10세" in desc
assert "19세" in desc