feat(saju-lab): daeun.py — 대운 8개 계산 (30/30 reference)
This commit is contained in:
107
saju-lab/tests/test_daeun.py
Normal file
107
saju-lab/tests/test_daeun.py
Normal 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
|
||||
Reference in New Issue
Block a user