feat(saju-lab): lucky.py — 럭키 컬러/숫자/방향 + 행운/위험 알림 (6 tests)
This commit is contained in:
84
saju-lab/app/calculator/lucky.py
Normal file
84
saju-lab/app/calculator/lucky.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""오늘의 럭키 컬러/숫자/방향 + 행운/위험 알림."""
|
||||||
|
from datetime import date as _date
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import sxtwl
|
||||||
|
|
||||||
|
from .constants import HEAVENLY_STEMS, EARTHLY_BRANCHES, FIVE_ELEMENTS, KE_CYCLE
|
||||||
|
|
||||||
|
|
||||||
|
LUCKY_COLOR_BY_ELEMENT: Dict[str, List[str]] = {
|
||||||
|
"木": ["청록", "녹색"],
|
||||||
|
"火": ["빨강", "주황"],
|
||||||
|
"土": ["황색", "베이지"],
|
||||||
|
"金": ["흰색", "은색"],
|
||||||
|
"水": ["파랑", "검정"],
|
||||||
|
}
|
||||||
|
|
||||||
|
LUCKY_DIRECTION_BY_ELEMENT: Dict[str, str] = {
|
||||||
|
"木": "동쪽",
|
||||||
|
"火": "남쪽",
|
||||||
|
"土": "중앙",
|
||||||
|
"金": "서쪽",
|
||||||
|
"水": "북쪽",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _day_stem_branch_idx(target: _date) -> tuple[int, int]:
|
||||||
|
"""양력 → sxtwl 일주 천간/지지 인덱스."""
|
||||||
|
day_obj = sxtwl.fromSolar(target.year, target.month, target.day)
|
||||||
|
try:
|
||||||
|
gz = day_obj.getDayGZ()
|
||||||
|
return gz.tg, gz.dz
|
||||||
|
except AttributeError:
|
||||||
|
return day_obj.getDayTG(), day_obj.getDayDZ()
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_lucky(saju: dict, analysis: dict, target_date: _date) -> dict:
|
||||||
|
"""오늘의 럭키 정보."""
|
||||||
|
yongshin = analysis["yong_shin"]["yong_shin"]
|
||||||
|
color = LUCKY_COLOR_BY_ELEMENT.get(yongshin, ["흰색"])
|
||||||
|
direction = LUCKY_DIRECTION_BY_ELEMENT.get(yongshin, "중앙")
|
||||||
|
|
||||||
|
# 럭키 숫자: 오늘 일진 천간+지지 인덱스 합 % 9 + 1 → 1~9
|
||||||
|
day_stem_idx, day_branch_idx = _day_stem_branch_idx(target_date)
|
||||||
|
number = (day_stem_idx + day_branch_idx) % 9 + 1
|
||||||
|
|
||||||
|
# 행운/위험
|
||||||
|
good_signs: List[str] = []
|
||||||
|
warnings: List[str] = []
|
||||||
|
|
||||||
|
today_stem = HEAVENLY_STEMS[day_stem_idx]
|
||||||
|
today_branch = EARTHLY_BRANCHES[day_branch_idx]
|
||||||
|
day_master = saju["day_stem"]
|
||||||
|
|
||||||
|
day_elem = FIVE_ELEMENTS[day_master]
|
||||||
|
today_elem = FIVE_ELEMENTS[today_stem]
|
||||||
|
if KE_CYCLE.get(day_elem) == today_elem:
|
||||||
|
good_signs.append("재물 기회가 다가옵니다")
|
||||||
|
if KE_CYCLE.get(today_elem) == day_elem:
|
||||||
|
warnings.append("강한 압박이 있을 수 있어요")
|
||||||
|
|
||||||
|
LIU_CHONG = {
|
||||||
|
frozenset(["子", "午"]), frozenset(["丑", "未"]),
|
||||||
|
frozenset(["寅", "申"]), frozenset(["卯", "酉"]),
|
||||||
|
frozenset(["辰", "戌"]), frozenset(["巳", "亥"]),
|
||||||
|
}
|
||||||
|
LIU_HE = {
|
||||||
|
frozenset(["子", "丑"]), frozenset(["寅", "亥"]),
|
||||||
|
frozenset(["卯", "戌"]), frozenset(["辰", "酉"]),
|
||||||
|
frozenset(["巳", "申"]), frozenset(["午", "未"]),
|
||||||
|
}
|
||||||
|
day_branch = saju["day"]["branch"]
|
||||||
|
if frozenset([day_branch, today_branch]) in LIU_CHONG:
|
||||||
|
warnings.append("대인 갈등에 주의하세요")
|
||||||
|
if frozenset([day_branch, today_branch]) in LIU_HE:
|
||||||
|
good_signs.append("좋은 인연을 만날 수 있어요")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"color": color,
|
||||||
|
"number": number,
|
||||||
|
"direction": direction,
|
||||||
|
"good_signs": good_signs,
|
||||||
|
"warnings": warnings,
|
||||||
|
}
|
||||||
63
saju-lab/tests/test_lucky.py
Normal file
63
saju-lab/tests/test_lucky.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.calculator.core import calculate_saju
|
||||||
|
from app.calculator.analysis import perform_full_analysis
|
||||||
|
from app.calculator.lucky import calculate_lucky
|
||||||
|
|
||||||
|
|
||||||
|
def _saju_for(year, month, day, hour, gender):
|
||||||
|
saju = calculate_saju(year, month, day, hour, gender)
|
||||||
|
analysis = perform_full_analysis(saju, 2026)
|
||||||
|
return saju, analysis
|
||||||
|
|
||||||
|
|
||||||
|
def test_lucky_keys():
|
||||||
|
saju, analysis = _saju_for(1990, 5, 15, 14, "male")
|
||||||
|
r = calculate_lucky(saju, analysis, date(2026, 5, 26))
|
||||||
|
for k in ("color", "number", "direction", "good_signs", "warnings"):
|
||||||
|
assert k in r, f"missing {k}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_lucky_number_range():
|
||||||
|
saju, analysis = _saju_for(1990, 5, 15, 14, "male")
|
||||||
|
r = calculate_lucky(saju, analysis, date(2026, 5, 26))
|
||||||
|
assert 1 <= r["number"] <= 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_lucky_color_from_yongshin():
|
||||||
|
saju, analysis = _saju_for(1990, 5, 15, 14, "male")
|
||||||
|
r = calculate_lucky(saju, analysis, date(2026, 5, 26))
|
||||||
|
assert isinstance(r["color"], list)
|
||||||
|
assert len(r["color"]) >= 1
|
||||||
|
yongshin = analysis["yong_shin"]["yong_shin"]
|
||||||
|
valid_colors_by_element = {
|
||||||
|
"木": {"청록", "녹색"},
|
||||||
|
"火": {"빨강", "주황"},
|
||||||
|
"土": {"황색", "베이지"},
|
||||||
|
"金": {"흰색", "은색"},
|
||||||
|
"水": {"파랑", "검정"},
|
||||||
|
}
|
||||||
|
expected_set = valid_colors_by_element[yongshin]
|
||||||
|
assert all(c in expected_set for c in r["color"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_lucky_direction_from_yongshin():
|
||||||
|
saju, analysis = _saju_for(1990, 5, 15, 14, "male")
|
||||||
|
r = calculate_lucky(saju, analysis, date(2026, 5, 26))
|
||||||
|
valid_dirs = {"동쪽", "남쪽", "중앙", "서쪽", "북쪽"}
|
||||||
|
assert r["direction"] in valid_dirs
|
||||||
|
|
||||||
|
|
||||||
|
def test_good_signs_and_warnings_are_lists():
|
||||||
|
saju, analysis = _saju_for(1990, 5, 15, 14, "male")
|
||||||
|
r = calculate_lucky(saju, analysis, date(2026, 5, 26))
|
||||||
|
assert isinstance(r["good_signs"], list)
|
||||||
|
assert isinstance(r["warnings"], list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handles_missing_hour():
|
||||||
|
saju, analysis = _saju_for(1990, 5, 15, None, "male")
|
||||||
|
r = calculate_lucky(saju, analysis, date(2026, 5, 26))
|
||||||
|
assert 1 <= r["number"] <= 9
|
||||||
Reference in New Issue
Block a user