feat(saju-lab): monthly_flow.py — 12개월 운세 흐름 (4 tests)
월간(月干)과 월지(月支)의 일간 관계를 이용한 12개월 운세 점수 계산: - 월간 상생(生) 관계: +5~10점 - 월간 상극(剋) 관계: -8점 - 월지 육합(六合) 관계: +10점 - 월지 육충(六衝) 관계: -12점 - 월지 상생/상극: ±4점 점수 범위 0~100, 5단계 레이블 (정체/도전/변동/안정/성장) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
saju-lab/app/calculator/monthly_flow.py
Normal file
91
saju-lab/app/calculator/monthly_flow.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""12개월 운세 흐름 — 월운(月運) + 일간 관계."""
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .constants import (
|
||||||
|
HEAVENLY_STEMS, EARTHLY_BRANCHES,
|
||||||
|
FIVE_ELEMENTS, SHENG_CYCLE, KE_CYCLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 寅월부터 시작 (1월=寅, 2월=卯, ..., 12월=丑)
|
||||||
|
MONTH_BRANCHES = ["寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥", "子", "丑"]
|
||||||
|
|
||||||
|
LIU_CHONG = {
|
||||||
|
frozenset(["子", "午"]), frozenset(["丑", "未"]),
|
||||||
|
frozenset(["寅", "申"]), frozenset(["卯", "酉"]),
|
||||||
|
frozenset(["辰", "戌"]), frozenset(["巳", "亥"]),
|
||||||
|
}
|
||||||
|
LIU_HE = {
|
||||||
|
frozenset(["子", "丑"]), frozenset(["寅", "亥"]),
|
||||||
|
frozenset(["卯", "戌"]), frozenset(["辰", "酉"]),
|
||||||
|
frozenset(["巳", "申"]), frozenset(["午", "未"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _month_stem_for_year(year_stem: str, branch_idx: int) -> str:
|
||||||
|
"""월간(月干) — saju-web의 공식: (yearStemIdx * 2 + branchIdx) % 10."""
|
||||||
|
year_stem_idx = HEAVENLY_STEMS.index(year_stem)
|
||||||
|
stem_idx = (year_stem_idx * 2 + branch_idx) % 10
|
||||||
|
return HEAVENLY_STEMS[stem_idx]
|
||||||
|
|
||||||
|
|
||||||
|
def _label_for(score: int) -> str:
|
||||||
|
if score >= 80:
|
||||||
|
return "성장"
|
||||||
|
if score >= 65:
|
||||||
|
return "안정"
|
||||||
|
if score >= 50:
|
||||||
|
return "변동"
|
||||||
|
if score >= 35:
|
||||||
|
return "도전"
|
||||||
|
return "정체"
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_monthly_flow(saju: dict, year: int) -> List[dict]:
|
||||||
|
"""12개월 운세 흐름."""
|
||||||
|
day_stem = saju["day_stem"]
|
||||||
|
day_element = FIVE_ELEMENTS[day_stem]
|
||||||
|
day_branch = saju["day"]["branch"]
|
||||||
|
year_stem = saju["year"]["stem"]
|
||||||
|
|
||||||
|
flow: List[dict] = []
|
||||||
|
for i, branch in enumerate(MONTH_BRANCHES):
|
||||||
|
branch_idx = EARTHLY_BRANCHES.index(branch)
|
||||||
|
stem = _month_stem_for_year(year_stem, branch_idx)
|
||||||
|
stem_element = FIVE_ELEMENTS[stem]
|
||||||
|
branch_element = FIVE_ELEMENTS[branch]
|
||||||
|
|
||||||
|
score = 60
|
||||||
|
|
||||||
|
if SHENG_CYCLE.get(day_element) == stem_element:
|
||||||
|
score += 5
|
||||||
|
elif SHENG_CYCLE.get(stem_element) == day_element:
|
||||||
|
score += 10
|
||||||
|
elif KE_CYCLE.get(day_element) == stem_element:
|
||||||
|
score += 8
|
||||||
|
elif KE_CYCLE.get(stem_element) == day_element:
|
||||||
|
score -= 8
|
||||||
|
elif stem_element == day_element:
|
||||||
|
score += 3
|
||||||
|
|
||||||
|
if frozenset([day_branch, branch]) in LIU_HE:
|
||||||
|
score += 10
|
||||||
|
elif frozenset([day_branch, branch]) in LIU_CHONG:
|
||||||
|
score -= 12
|
||||||
|
|
||||||
|
if SHENG_CYCLE.get(branch_element) == day_element:
|
||||||
|
score += 4
|
||||||
|
elif KE_CYCLE.get(branch_element) == day_element:
|
||||||
|
score -= 4
|
||||||
|
|
||||||
|
score = max(0, min(100, score))
|
||||||
|
|
||||||
|
flow.append({
|
||||||
|
"month": i + 1,
|
||||||
|
"stem": stem,
|
||||||
|
"branch": branch,
|
||||||
|
"score": score,
|
||||||
|
"label": _label_for(score),
|
||||||
|
})
|
||||||
|
|
||||||
|
return flow
|
||||||
38
saju-lab/tests/test_monthly_flow.py
Normal file
38
saju-lab/tests/test_monthly_flow.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.calculator.core import calculate_saju
|
||||||
|
from app.calculator.monthly_flow import calculate_monthly_flow
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_12_entries():
|
||||||
|
saju = calculate_saju(1990, 5, 15, 14, "male")
|
||||||
|
flow = calculate_monthly_flow(saju, 2026)
|
||||||
|
assert len(flow) == 12
|
||||||
|
|
||||||
|
|
||||||
|
def test_entries_have_required_keys():
|
||||||
|
saju = calculate_saju(1990, 5, 15, 14, "male")
|
||||||
|
flow = calculate_monthly_flow(saju, 2026)
|
||||||
|
for i, entry in enumerate(flow):
|
||||||
|
assert entry["month"] == i + 1
|
||||||
|
for k in ("stem", "branch", "score", "label"):
|
||||||
|
assert k in entry, f"month {i+1} missing {k}"
|
||||||
|
assert 0 <= entry["score"] <= 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_labels_are_valid():
|
||||||
|
saju = calculate_saju(1990, 5, 15, 14, "male")
|
||||||
|
flow = calculate_monthly_flow(saju, 2026)
|
||||||
|
valid_labels = {"변동", "성장", "안정", "도전", "정체"}
|
||||||
|
for entry in flow:
|
||||||
|
assert entry["label"] in valid_labels
|
||||||
|
|
||||||
|
|
||||||
|
def test_different_sajus_different_flows():
|
||||||
|
s1 = calculate_saju(1990, 5, 15, 14, "male")
|
||||||
|
s2 = calculate_saju(1985, 1, 1, 0, "female")
|
||||||
|
f1 = calculate_monthly_flow(s1, 2026)
|
||||||
|
f2 = calculate_monthly_flow(s2, 2026)
|
||||||
|
scores_1 = [e["score"] for e in f1]
|
||||||
|
scores_2 = [e["score"] for e in f2]
|
||||||
|
assert scores_1 != scores_2
|
||||||
Reference in New Issue
Block a user