157 lines
5.7 KiB
Python
157 lines
5.7 KiB
Python
"""shinsal.py — 지장간/신살/공망/지지 상호작용 검증.
|
|
|
|
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.shinsal import (
|
|
get_hidden_stems,
|
|
get_all_hidden_stems,
|
|
analyze_branch_interactions,
|
|
calculate_shinsal,
|
|
calculate_gongmang,
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
def _canonical(item):
|
|
"""dict/list 비교용 — 안정 정렬을 위한 JSON 직렬화."""
|
|
return json.dumps(item, ensure_ascii=False, sort_keys=True)
|
|
|
|
|
|
# ─── 1. 지장간 단순 테스트 ────────────────────────────────────────────
|
|
def test_hidden_stems_basic():
|
|
assert get_hidden_stems("子") == ["癸"]
|
|
assert get_hidden_stems("丑") == ["己", "癸", "辛"]
|
|
assert get_hidden_stems("寅") == ["甲", "丙", "戊"]
|
|
assert get_hidden_stems("卯") == ["乙"]
|
|
assert get_hidden_stems("辰") == ["戊", "乙", "癸"]
|
|
assert get_hidden_stems("巳") == ["丙", "庚", "戊"]
|
|
assert get_hidden_stems("午") == ["丁", "己"]
|
|
assert get_hidden_stems("未") == ["己", "丁", "乙"]
|
|
assert get_hidden_stems("申") == ["庚", "壬", "戊"]
|
|
assert get_hidden_stems("酉") == ["辛"]
|
|
assert get_hidden_stems("戌") == ["戊", "辛", "丁"]
|
|
assert get_hidden_stems("亥") == ["壬", "甲"]
|
|
|
|
|
|
# ─── 2. hiddenStems reference 매칭 ───────────────────────────────────
|
|
@pytest.mark.parametrize(
|
|
"case",
|
|
REF,
|
|
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
|
|
)
|
|
def test_hidden_stems_match_reference(case):
|
|
inp = case["input"]
|
|
expected_raw = case["expected"]["analysis"].get("hiddenStems")
|
|
if expected_raw is None:
|
|
pytest.skip("no hiddenStems in expected")
|
|
expected = _normalize(expected_raw)
|
|
|
|
saju = calculate_saju(
|
|
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
|
|
)
|
|
actual = get_all_hidden_stems(saju)
|
|
assert actual == expected, f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
|
|
|
|
|
|
# ─── 3. branchInteractions reference 매칭 ────────────────────────────
|
|
@pytest.mark.parametrize(
|
|
"case",
|
|
REF,
|
|
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
|
|
)
|
|
def test_branch_interactions_match_reference(case):
|
|
inp = case["input"]
|
|
expected_raw = case["expected"]["analysis"].get("branchInteractions")
|
|
if expected_raw is None:
|
|
pytest.skip("no branchInteractions")
|
|
expected = _normalize(expected_raw)
|
|
|
|
saju = calculate_saju(
|
|
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
|
|
)
|
|
actual = analyze_branch_interactions(saju)
|
|
# 순서가 다를 수 있으므로 정렬 후 비교
|
|
assert sorted(map(_canonical, actual)) == sorted(map(_canonical, expected)), (
|
|
f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
|
|
)
|
|
|
|
|
|
# ─── 4. shinsal reference 매칭 ───────────────────────────────────────
|
|
@pytest.mark.parametrize(
|
|
"case",
|
|
REF,
|
|
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
|
|
)
|
|
def test_shinsal_match_reference(case):
|
|
inp = case["input"]
|
|
expected_raw = case["expected"]["analysis"].get("shinsal")
|
|
if expected_raw is None:
|
|
pytest.skip("no shinsal")
|
|
expected = _normalize(expected_raw)
|
|
|
|
saju = calculate_saju(
|
|
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
|
|
)
|
|
actual = calculate_shinsal(saju)
|
|
assert sorted(map(_canonical, actual)) == sorted(map(_canonical, expected)), (
|
|
f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
|
|
)
|
|
|
|
|
|
# ─── 5. gongmang reference 매칭 ──────────────────────────────────────
|
|
@pytest.mark.parametrize(
|
|
"case",
|
|
REF,
|
|
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
|
|
)
|
|
def test_gongmang_match_reference(case):
|
|
inp = case["input"]
|
|
expected_raw = case["expected"]["analysis"].get("gongmang")
|
|
if expected_raw is None:
|
|
pytest.skip("no gongmang")
|
|
expected = _normalize(expected_raw)
|
|
|
|
saju = calculate_saju(
|
|
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
|
|
)
|
|
actual = calculate_gongmang(saju["day_stem"], saju["day"]["branch"])
|
|
|
|
# branches / branches_kr / description 전체 비교
|
|
assert actual.get("branches") == expected.get("branches"), (
|
|
f"branches mismatch for {inp}\nactual={actual}\nexpected={expected}"
|
|
)
|
|
assert actual.get("branches_kr") == expected.get("branches_kr"), (
|
|
f"branches_kr mismatch for {inp}\nactual={actual}\nexpected={expected}"
|
|
)
|
|
assert actual.get("description") == expected.get("description"), (
|
|
f"description mismatch for {inp}\nactual={actual}\nexpected={expected}"
|
|
)
|