公众授权版:normal · 1000 次 · 有效期至 2026-12-31
副本编号 LY-20261007-AC63(水印指纹已嵌入) 含 README、.gitignore;排除 .DS_Store / __pycache__ / .pytest_cache
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""起运与大运(文档 4.8 节)。
|
||||
|
||||
规则(真诠体系):
|
||||
- 阳男阴女顺排,阴男阳女逆排
|
||||
- 起运:顺排取出生时刻到下一节、逆排取到上一节的时间差
|
||||
- 折算:3 天 = 1 年(1 年 = 12 个月 = 360 天)
|
||||
- 大运每步 10 年,从月柱干支起顺/逆推
|
||||
|
||||
纯 Python 历法推算,不 import tyme4py。
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from .model import LuckPillar
|
||||
|
||||
GAN = "甲乙丙丁戊己庚辛壬癸"
|
||||
ZHI = "子丑寅卯辰巳午未申酉戌亥"
|
||||
|
||||
YANG_GAN = frozenset("甲丙戊庚壬")
|
||||
|
||||
# 年上起月法(五虎遁):年干 → 寅月天干
|
||||
# 甲己之年丙作首,乙庚之年戊为头,丙辛之年寻庚起,丁壬壬寅顺水流,戊癸甲寅
|
||||
_MONTH_HEAD_GAN = {
|
||||
"甲": "丙", "己": "丙",
|
||||
"乙": "戊", "庚": "戊",
|
||||
"丙": "庚", "辛": "庚",
|
||||
"丁": "壬", "壬": "壬",
|
||||
"戊": "甲", "癸": "甲",
|
||||
}
|
||||
|
||||
# 节气月:月支从寅起(立春为寅月),(月支, 起始节气)
|
||||
_SOLAR_MONTHS = (
|
||||
("寅", "立春"), ("卯", "惊蛰"), ("辰", "清明"), ("巳", "立夏"),
|
||||
("午", "芒种"), ("未", "小暑"), ("申", "立秋"), ("酉", "白露"),
|
||||
("戌", "寒露"), ("亥", "立冬"), ("子", "大雪"), ("丑", "小寒"),
|
||||
)
|
||||
|
||||
SECONDS_PER_DAY = 86400
|
||||
DAYS_PER_LUCK_YEAR = 3.0 # 3 天折 1 年
|
||||
|
||||
|
||||
def ganzhi_of_year(y: int) -> str:
|
||||
"""公元 4 年为甲子,(y-4)%60 定位。"""
|
||||
return GAN[(y - 4) % 10] + ZHI[(y - 4) % 12]
|
||||
|
||||
|
||||
def ganzhi_cycle_next(gan: str, zhi: str, n: int) -> tuple[str, str]:
|
||||
"""干支同步推进 n 步(n 可为负)。"""
|
||||
return GAN[(GAN.index(gan) + n) % 10], ZHI[(ZHI.index(zhi) + n) % 12]
|
||||
|
||||
|
||||
def is_forward(gender: str, year_gan: str) -> bool:
|
||||
"""阳年干(甲丙戊庚壬)+ 男 → 顺;阴年干 + 女 → 顺。"""
|
||||
return (year_gan in YANG_GAN) == (gender == "男")
|
||||
|
||||
|
||||
def compute_luck(
|
||||
birth_tst: datetime,
|
||||
prev_jie_time: datetime,
|
||||
next_jie_time: datetime,
|
||||
forward: bool,
|
||||
) -> dict:
|
||||
"""起运:顺排取下一节减出生,逆排取出生减上一节;3 天折 1 年。
|
||||
|
||||
返回 {"is_forward", "start_age_years", "start_age_text"}。
|
||||
start_age_years 保留 4 位小数;起运年龄精度要到「天」级,否则起运年份会差一年。
|
||||
"""
|
||||
if forward:
|
||||
delta = next_jie_time - birth_tst
|
||||
else:
|
||||
delta = birth_tst - prev_jie_time
|
||||
|
||||
start_age_years = delta.total_seconds() / SECONDS_PER_DAY / DAYS_PER_LUCK_YEAR
|
||||
|
||||
return {
|
||||
"is_forward": forward,
|
||||
"start_age_years": round(start_age_years, 4),
|
||||
"start_age_text": _age_to_text(start_age_years),
|
||||
}
|
||||
|
||||
|
||||
def _age_to_text(years: float) -> str:
|
||||
"""年数 → 「X 岁 Y 个月 Z 天」。
|
||||
|
||||
按 1 年 = 12 个月 = 360 天折算(0.5 年 = 6 个月),四舍五入到天。
|
||||
"""
|
||||
total_days = round(years * 360)
|
||||
y = total_days // 360
|
||||
m = (total_days % 360) // 30
|
||||
d = total_days % 30
|
||||
return f"{y} 岁 {m} 个月 {d} 天"
|
||||
|
||||
|
||||
def decade_pillars(
|
||||
month_gan: str,
|
||||
month_zhi: str,
|
||||
forward: bool,
|
||||
start_age_years: float,
|
||||
birth_year: int,
|
||||
n: int = 8,
|
||||
) -> list[LuckPillar]:
|
||||
"""从月柱起顺/逆推 n 步大运(第 1 步为月柱 ±1)。"""
|
||||
step = 1 if forward else -1
|
||||
pillars = []
|
||||
for index in range(n):
|
||||
gan, zhi = ganzhi_cycle_next(month_gan, month_zhi, step * (index + 1))
|
||||
start_age = start_age_years + 10 * index
|
||||
pillars.append(LuckPillar(
|
||||
index=index + 1,
|
||||
gan=gan,
|
||||
zhi=zhi,
|
||||
start_age=round(start_age, 4),
|
||||
start_year=birth_year + round(start_age),
|
||||
))
|
||||
return pillars
|
||||
|
||||
|
||||
def flow_years(current_year: int, n: int = 10) -> list[tuple[int, str]]:
|
||||
"""从今年起 n 个流年干支。"""
|
||||
return [(y, ganzhi_of_year(y)) for y in range(current_year, current_year + n)]
|
||||
|
||||
|
||||
def flow_months(year: int) -> list[tuple[str, str]]:
|
||||
"""该年 12 个节气月,返回 (「X月(节气起)」, 干支)。
|
||||
|
||||
月支从寅起(立春为寅月);月干按年上起月法(五虎遁)。
|
||||
"""
|
||||
year_gan = GAN[(year - 4) % 10]
|
||||
head = GAN.index(_MONTH_HEAD_GAN[year_gan])
|
||||
return [
|
||||
(f"{zhi}月({jie}起)", GAN[(head + i) % 10] + zhi)
|
||||
for i, (zhi, jie) in enumerate(_SOLAR_MONTHS)
|
||||
]
|
||||
@@ -0,0 +1,151 @@
|
||||
"""L1→L2 数据契约(文档 4.7 节)。
|
||||
|
||||
改动本文件等于改契约版本(schema_version)。
|
||||
本模块只做数据定义与组装,不含任何命理判断(P1)。
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pillar:
|
||||
gan: str
|
||||
zhi: str
|
||||
gan_ten_god: str | None = None # 该柱天干相对日干的十神
|
||||
hidden_stems: list[str] = field(default_factory=list) # 藏干
|
||||
hidden_ten_gods: list[str] = field(default_factory=list)
|
||||
na_yin: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LuckPillar:
|
||||
index: int
|
||||
gan: str
|
||||
zhi: str
|
||||
start_age: float
|
||||
start_year: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChartData:
|
||||
# —— 输入 ——
|
||||
name: str
|
||||
gender: str
|
||||
birthplace: str
|
||||
birth_clock: str # 用户原始输入
|
||||
|
||||
# —— 时间归算审计(P3)——
|
||||
tz_used: str
|
||||
is_dst: bool
|
||||
lon_correction_min: float
|
||||
eot_minutes: float
|
||||
true_solar_time: str
|
||||
is_late_zi: bool
|
||||
|
||||
# —— 四柱 ——
|
||||
year: Pillar
|
||||
month: Pillar
|
||||
day: Pillar
|
||||
hour: Pillar
|
||||
|
||||
# —— 派生 ——
|
||||
day_master: str # 日干
|
||||
month_ling: str # 月令地支(取格之锚)
|
||||
solar_term_before: str # 出生前一个节气
|
||||
solar_term_after: str # 出生后一个节气
|
||||
luck_pillars: list[LuckPillar] = field(default_factory=list)
|
||||
|
||||
# —— 派生(渲染层补充字段)——
|
||||
luck_is_forward: bool = False # 大运顺排/逆排
|
||||
start_age_text: str = "" # 起运年龄,如「7 岁 7 个月 19 天」
|
||||
terrain: dict[str, str] = field(default_factory=dict) # 四柱键 → 十二长生
|
||||
void: dict[str, str] = field(default_factory=dict) # {"day": .., "hour": ..}
|
||||
shen_sha: list[str] = field(default_factory=list) # 神煞
|
||||
flow_years: list[tuple[int, str]] = field(default_factory=list) # 流年 (年份, 干支)
|
||||
flow_months: list[tuple[str, str]] = field(default_factory=list) # 流月 (标签, 干支)
|
||||
tai_yuan: str = "" # 胎元
|
||||
ming_gong: str = "" # 命宫
|
||||
|
||||
# —— 溯源 ——
|
||||
library_version: str = "" # tyme4py 版本,写入输出
|
||||
schema_version: str = "1.0"
|
||||
|
||||
|
||||
def _fmt_dt(dt: datetime) -> str:
|
||||
return dt.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def _fmt_term(term: dict) -> str:
|
||||
"""{"name", "time"} → 「节气名 YYYY-MM-DD HH:MM」"""
|
||||
return f"{term['name']} {_fmt_dt(term['time'])}"
|
||||
|
||||
|
||||
def _build_pillar(key: str, raw: dict) -> Pillar:
|
||||
gz = raw[key]
|
||||
hidden_pairs = raw.get("hidden", {}).get(key, [])
|
||||
return Pillar(
|
||||
gan=gz["gan"],
|
||||
zhi=gz["zhi"],
|
||||
gan_ten_god=raw.get("ten_god", {}).get(key),
|
||||
hidden_stems=[pair[0] for pair in hidden_pairs],
|
||||
hidden_ten_gods=[pair[1] for pair in hidden_pairs],
|
||||
na_yin=raw.get("na_yin", {}).get(key),
|
||||
)
|
||||
|
||||
|
||||
def build_chart_data(
|
||||
name: str,
|
||||
gender: str,
|
||||
place: str,
|
||||
clock: str,
|
||||
loc,
|
||||
tz_audit: dict,
|
||||
st_audit: dict,
|
||||
bd_audit: dict,
|
||||
raw: dict,
|
||||
luck: dict,
|
||||
luck_pillars: list[LuckPillar],
|
||||
flow_years: list[tuple[int, str]],
|
||||
flow_months: list[tuple[str, str]],
|
||||
) -> ChartData:
|
||||
"""把 tyme_adapter 的原始 dict、各层 audit 与 luck 结果组装成 ChartData。
|
||||
|
||||
纯组装,零命理判断。
|
||||
"""
|
||||
return ChartData(
|
||||
# 输入
|
||||
name=name,
|
||||
gender=gender,
|
||||
birthplace=place,
|
||||
birth_clock=clock,
|
||||
# 时间归算审计
|
||||
tz_used=tz_audit["tz_used"],
|
||||
is_dst=tz_audit["is_dst"],
|
||||
lon_correction_min=st_audit["lon_correction_min"],
|
||||
eot_minutes=st_audit["eot_minutes"],
|
||||
true_solar_time=_fmt_dt(st_audit["tst"]),
|
||||
is_late_zi=bd_audit["is_late_zi"],
|
||||
# 四柱
|
||||
year=_build_pillar("year", raw),
|
||||
month=_build_pillar("month", raw),
|
||||
day=_build_pillar("day", raw),
|
||||
hour=_build_pillar("hour", raw),
|
||||
# 派生
|
||||
day_master=raw["day_master"],
|
||||
month_ling=raw["month"]["zhi"],
|
||||
solar_term_before=_fmt_term(raw["prev_jie"]),
|
||||
solar_term_after=_fmt_term(raw["next_jie"]),
|
||||
luck_pillars=luck_pillars,
|
||||
# 派生(补充字段)
|
||||
luck_is_forward=luck["is_forward"],
|
||||
start_age_text=luck["start_age_text"],
|
||||
terrain=raw.get("terrain", {}),
|
||||
void=raw.get("void", {}),
|
||||
shen_sha=raw.get("shen_sha", []),
|
||||
flow_years=flow_years,
|
||||
flow_months=flow_months,
|
||||
tai_yuan=raw.get("tai_yuan", ""),
|
||||
ming_gong=raw.get("ming_gong", ""),
|
||||
# 溯源
|
||||
library_version=raw.get("library_version", ""),
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""tyme4py 唯一接触面:输入真太阳时时刻,返回四柱及派生信息(纯计算,无命理判断)。"""
|
||||
from datetime import datetime
|
||||
|
||||
import tyme4py
|
||||
from tyme4py.solar import SolarTime, SolarTerm
|
||||
|
||||
LIBRARY_VERSION = tyme4py.__version__
|
||||
|
||||
_PILLARS = ("year", "month", "day", "hour")
|
||||
|
||||
|
||||
def _solar_time_to_dt(t: SolarTime) -> datetime:
|
||||
return datetime(t.get_year(), t.get_month(), t.get_day(),
|
||||
t.get_hour(), t.get_minute(), t.get_second())
|
||||
|
||||
|
||||
def _jie_bounds(cast_dt: datetime) -> tuple:
|
||||
"""出生时刻前后最近的两个节令(is_jie()),以出生年为中心 ±1 年遍历。"""
|
||||
prev_jie = next_jie = None
|
||||
for y in (cast_dt.year - 1, cast_dt.year, cast_dt.year + 1):
|
||||
for i in range(24):
|
||||
term = SolarTerm.from_index(y, i)
|
||||
if not term.is_jie():
|
||||
continue
|
||||
t = _solar_time_to_dt(term.get_julian_day().get_solar_time())
|
||||
if t < cast_dt and (prev_jie is None or t > prev_jie[1]):
|
||||
prev_jie = (term, t)
|
||||
elif t > cast_dt and (next_jie is None or t < next_jie[1]):
|
||||
next_jie = (term, t)
|
||||
return prev_jie, next_jie
|
||||
|
||||
|
||||
def _hidden(me, earth_branch) -> list:
|
||||
items = []
|
||||
for getter in ("get_hide_heaven_stem_main",
|
||||
"get_hide_heaven_stem_middle",
|
||||
"get_hide_heaven_stem_residual"):
|
||||
hs = getattr(earth_branch, getter)()
|
||||
if hs is not None:
|
||||
items.append([hs.get_name(), me.get_ten_star(hs).get_name()])
|
||||
return items
|
||||
|
||||
|
||||
def _shen_sha(solar: SolarTime) -> list:
|
||||
try:
|
||||
return [g.get_name() for g in solar.get_lunar_hour().get_lunar_day().get_gods()]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def cast(cast_dt: datetime, gender: str) -> dict:
|
||||
"""cast_dt:已完成子初换日调整的真太阳时(naive,北京时间语义)。"""
|
||||
solar = SolarTime.from_ymd_hms(cast_dt.year, cast_dt.month, cast_dt.day,
|
||||
cast_dt.hour, cast_dt.minute, cast_dt.second)
|
||||
ec = solar.get_lunar_hour().get_eight_char()
|
||||
pillars = {k: getattr(ec, f"get_{k}")() for k in _PILLARS}
|
||||
me = ec.get_day().get_heaven_stem()
|
||||
|
||||
prev_jie, next_jie = _jie_bounds(cast_dt)
|
||||
|
||||
return {
|
||||
**{k: {"gan": p.get_heaven_stem().get_name(),
|
||||
"zhi": p.get_earth_branch().get_name()} for k, p in pillars.items()},
|
||||
"day_master": me.get_name(),
|
||||
"ten_god": {k: me.get_ten_star(pillars[k].get_heaven_stem()).get_name()
|
||||
for k in ("year", "month", "hour")},
|
||||
"hidden": {k: _hidden(me, pillars[k].get_earth_branch()) for k in _PILLARS},
|
||||
"terrain": {k: me.get_terrain(pillars[k].get_earth_branch()).get_name()
|
||||
for k in _PILLARS},
|
||||
"na_yin": {k: pillars[k].get_sound().get_name() for k in _PILLARS},
|
||||
"void": {k: "".join(b.get_name() for b in pillars[k].get_extra_earth_branches())
|
||||
for k in ("day", "hour")},
|
||||
"shen_sha": _shen_sha(solar),
|
||||
"prev_jie": {"name": prev_jie[0].get_name(), "time": prev_jie[1]},
|
||||
"next_jie": {"name": next_jie[0].get_name(), "time": next_jie[1]},
|
||||
"tai_yuan": ec.get_fetal_origin().get_name(),
|
||||
"ming_gong": ec.get_own_sign().get_name(),
|
||||
"library_version": LIBRARY_VERSION,
|
||||
"raw": ec,
|
||||
}
|
||||
Reference in New Issue
Block a user