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