150 lines
5.9 KiB
Python
150 lines
5.9 KiB
Python
from datetime import datetime
|
||
|
||
try:
|
||
from lunar_python import Lunar, Solar
|
||
except ImportError: # pragma: no cover - production image installs the package
|
||
Lunar = None
|
||
Solar = None
|
||
|
||
|
||
STEM_WUXING = {
|
||
"甲": "木",
|
||
"乙": "木",
|
||
"丙": "火",
|
||
"丁": "火",
|
||
"戊": "土",
|
||
"己": "土",
|
||
"庚": "金",
|
||
"辛": "金",
|
||
"壬": "水",
|
||
"癸": "水",
|
||
}
|
||
|
||
BRANCH_WUXING = {
|
||
"子": "水",
|
||
"丑": "土",
|
||
"寅": "木",
|
||
"卯": "木",
|
||
"辰": "土",
|
||
"巳": "火",
|
||
"午": "火",
|
||
"未": "土",
|
||
"申": "金",
|
||
"酉": "金",
|
||
"戌": "土",
|
||
"亥": "水",
|
||
}
|
||
|
||
|
||
class BaziCalculator:
|
||
def calculate(self, payload: dict) -> dict:
|
||
birth_date = payload["birth_date"]
|
||
time_unknown = bool(payload.get("time_unknown"))
|
||
birth_time = payload.get("birth_time") if not time_unknown else "12:00"
|
||
hour, minute = self._parse_time(birth_time or "12:00")
|
||
year, month, day = self._parse_date(birth_date)
|
||
calendar_type = payload.get("calendar_type", "solar")
|
||
|
||
if Solar and Lunar:
|
||
if calendar_type == "lunar":
|
||
lunar_month = -month if payload.get("is_leap_month") else month
|
||
lunar = Lunar.fromYmdHms(year, lunar_month, day, hour, minute, 0)
|
||
solar = lunar.getSolar()
|
||
else:
|
||
solar = Solar.fromYmdHms(year, month, day, hour, minute, 0)
|
||
lunar = solar.getLunar()
|
||
eight = lunar.getEightChar()
|
||
pillars = {
|
||
"year": eight.getYear(),
|
||
"month": eight.getMonth(),
|
||
"day": eight.getDay(),
|
||
"time": eight.getTime() if not time_unknown else "时辰不详",
|
||
}
|
||
return {
|
||
"calendar_type": calendar_type,
|
||
"is_leap_month": bool(payload.get("is_leap_month")),
|
||
"birth_date": birth_date,
|
||
"birth_time": None if time_unknown else f"{hour:02d}:{minute:02d}",
|
||
"time_unknown": time_unknown,
|
||
"birth_place": payload.get("birth_place"),
|
||
"nickname": payload.get("nickname"),
|
||
"gender": payload.get("gender"),
|
||
"solar_date": f"{solar.getYear():04d}-{solar.getMonth():02d}-{solar.getDay():02d}",
|
||
"lunar_date": f"{lunar.getYear()}年{lunar.getMonth()}月{lunar.getDay()}日",
|
||
"pillars": pillars,
|
||
"wuxing": {
|
||
"year": eight.getYearWuXing(),
|
||
"month": eight.getMonthWuXing(),
|
||
"day": eight.getDayWuXing(),
|
||
"time": eight.getTimeWuXing() if not time_unknown else "时辰不详",
|
||
},
|
||
"shi_shen": {
|
||
"year": eight.getYearShiShenGan(),
|
||
"month": eight.getMonthShiShenGan(),
|
||
"day": "日主",
|
||
"time": eight.getTimeShiShenGan() if not time_unknown else "时辰不详",
|
||
},
|
||
"wuxing_balance": self._count_wuxing(pillars),
|
||
"day_master": eight.getDayGan(),
|
||
"chart_notes": [
|
||
"出生地仅用于报告语境,不参与经度校正。",
|
||
"时辰不详时,时柱相关判断会降低权重。",
|
||
]
|
||
if time_unknown
|
||
else ["出生地仅用于报告语境,不参与经度校正。"],
|
||
}
|
||
|
||
return self._fallback_chart(payload, year, month, day, hour, minute, time_unknown)
|
||
|
||
def _fallback_chart(self, payload: dict, year: int, month: int, day: int, hour: int, minute: int, time_unknown: bool) -> dict:
|
||
stems = "甲乙丙丁戊己庚辛壬癸"
|
||
branches = "子丑寅卯辰巳午未申酉戌亥"
|
||
seed = year * 372 + month * 31 + day + hour
|
||
|
||
def pillar(offset: int) -> str:
|
||
return stems[(seed + offset) % 10] + branches[(seed + offset) % 12]
|
||
|
||
pillars = {
|
||
"year": pillar(0),
|
||
"month": pillar(13),
|
||
"day": pillar(27),
|
||
"time": pillar(41) if not time_unknown else "时辰不详",
|
||
}
|
||
return {
|
||
"calendar_type": payload.get("calendar_type", "solar"),
|
||
"is_leap_month": bool(payload.get("is_leap_month")),
|
||
"birth_date": payload["birth_date"],
|
||
"birth_time": None if time_unknown else f"{hour:02d}:{minute:02d}",
|
||
"time_unknown": time_unknown,
|
||
"birth_place": payload.get("birth_place"),
|
||
"nickname": payload.get("nickname"),
|
||
"gender": payload.get("gender"),
|
||
"solar_date": payload["birth_date"],
|
||
"lunar_date": "本地未安装 lunar_python,暂用娱乐排盘",
|
||
"pillars": pillars,
|
||
"wuxing": {"year": "参考", "month": "参考", "day": "参考", "time": "参考"},
|
||
"shi_shen": {"year": "参考", "month": "参考", "day": "日主", "time": "参考"},
|
||
"wuxing_balance": self._count_wuxing(pillars),
|
||
"day_master": pillar(27)[0],
|
||
"chart_notes": ["本地未安装 lunar_python,当前为兜底娱乐排盘。"],
|
||
}
|
||
|
||
def _parse_date(self, value: str) -> tuple[int, int, int]:
|
||
parsed = datetime.strptime(value, "%Y-%m-%d")
|
||
return parsed.year, parsed.month, parsed.day
|
||
|
||
def _parse_time(self, value: str) -> tuple[int, int]:
|
||
parsed = datetime.strptime(value, "%H:%M")
|
||
return parsed.hour, parsed.minute
|
||
|
||
def _count_wuxing(self, pillars: dict) -> dict:
|
||
counts = {"木": 0, "火": 0, "土": 0, "金": 0, "水": 0}
|
||
for pillar in pillars.values():
|
||
if not isinstance(pillar, str) or pillar == "时辰不详":
|
||
continue
|
||
for char in pillar:
|
||
element = STEM_WUXING.get(char) or BRANCH_WUXING.get(char)
|
||
if element:
|
||
counts[element] += 1
|
||
return counts
|