2602 lines
119 KiB
Python
2602 lines
119 KiB
Python
"""Six parity chapters for issue #91 section A.
|
||
|
||
Rows are read from the public packet. Missing labels raise. Raw keys and
|
||
C-class sections are not printed. Chinese sentences are written here.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
try:
|
||
from scripts.report_render.appendix_language import (
|
||
friendship_table,
|
||
planet_list,
|
||
planet_name,
|
||
sign_name,
|
||
undetermined,
|
||
yes_no,
|
||
)
|
||
from scripts.report_render.text import SIGNS
|
||
except ModuleNotFoundError: # pragma: no cover
|
||
from report_render.appendix_language import (
|
||
friendship_table,
|
||
planet_list,
|
||
planet_name,
|
||
sign_name,
|
||
undetermined,
|
||
yes_no,
|
||
)
|
||
from report_render.text import SIGNS
|
||
|
||
LABELS: dict[str, tuple[str, str]] = {
|
||
"age": ("Age", "岁数"),
|
||
"anchor": ("Anchor", "锚点"),
|
||
"applicable": ("Applicable", "是否适用"),
|
||
"aspect": ("Aspect", "相位"),
|
||
"atmakaraka": ("Atmakaraka", "灵魂指示星"),
|
||
"bhava_house": ("Bhava house", "Bhava 宫"),
|
||
"boundary": ("Boundary", "交界"),
|
||
"candidates": ("Candidates", "候选"),
|
||
"category": ("Category", "类别"),
|
||
"component": ("Component", "分量"),
|
||
"compound": ("Compound relation", "复合关系"),
|
||
"counted": ("Counted", "计入"),
|
||
"date": ("Date", "日期"),
|
||
"days": ("Days from reference", "距参考日天数"),
|
||
"declination": ("Declination", "赤纬"),
|
||
"degree": ("Degree in sign", "落座度数"),
|
||
"detected": ("Detected", "是否成立"),
|
||
"dignity": ("Dignity", "尊贵"),
|
||
"domain": ("Domain", "领域"),
|
||
"end": ("End", "结束"),
|
||
"example_total": ("Example total", "例题合计"),
|
||
"friends": ("Natural friends", "天然友"),
|
||
"enemies": ("Natural enemies", "天然敌"),
|
||
"neutral": ("Natural neutral", "天然中性"),
|
||
"temporary_friends": ("Temporary friends", "临时友"),
|
||
"temporary_enemies": ("Temporary enemies", "临时敌"),
|
||
"house": ("House", "宫位"),
|
||
"item": ("Item", "项目"),
|
||
"karaka": ("Karaka", "卡拉卡"),
|
||
"level": ("Level", "层级"),
|
||
"lord": ("Lord", "主星"),
|
||
"longitude": ("Longitude", "黄经"),
|
||
"method": ("Method", "口径"),
|
||
"months": ("Months", "月数"),
|
||
"name": ("Name", "名称"),
|
||
"nakshatra": ("Nakshatra", "星宿"),
|
||
"occupant": ("Occupants", "落入行星"),
|
||
"orb": ("Orb days", "容许天数"),
|
||
"order": ("Order", "顺序"),
|
||
"pada": ("Pada", "星宿四分"),
|
||
"permanent": ("Permanent", "固定分"),
|
||
"phase": ("Phase", "阶段"),
|
||
"planet": ("Planet", "行星"),
|
||
"point": ("Point", "点"),
|
||
"policy": ("Node policy", "交点口径"),
|
||
"rashi_house": ("Rashi house", "星座宫"),
|
||
"reference": ("Reference date", "参考日"),
|
||
"role": ("Role", "角色"),
|
||
"rule": ("Rule", "规则"),
|
||
"score": ("Score", "分数"),
|
||
"sign": ("Sign", "星座"),
|
||
"speed": ("Speed", "速度"),
|
||
"start": ("Start", "开始"),
|
||
"status": ("Status", "状态"),
|
||
"system": ("System", "系统"),
|
||
"topic": ("Topic", "项目"),
|
||
"total": ("Total", "合计"),
|
||
"value": ("Value", "内容"),
|
||
"years": ("Years", "年数"),
|
||
"full_years": ("Full years", "完整年数"),
|
||
"balance_years": ("Balance years", "余额年数"),
|
||
"elapsed_at_birth": ("Elapsed at birth", "出生时已过年数"),
|
||
"rashi": ("Rashi", "星座"),
|
||
"start_date": ("Start", "开始"),
|
||
"end_date": ("End", "结束"),
|
||
"yogini": ("Yogini", "Yogini"),
|
||
"progressed_longitude": ("Progressed longitude", "推进黄经"),
|
||
"progressed_sign": ("Progressed sign", "推进星座"),
|
||
"progressed_nakshatra": ("Progressed nakshatra", "推进星宿"),
|
||
"progressed_nakshatra_lord": ("Nakshatra lord", "星宿主星"),
|
||
"progressed_nakshatra_pada": ("Pada", "星宿四分"),
|
||
"progression_rate": ("Progression rate", "推进速率"),
|
||
"years_elapsed": ("Years elapsed", "已过年数"),
|
||
"degree_in_sign": ("Degree in sign", "落座度数"),
|
||
"adhipathi": ("Adhipathi", "宫主力量"),
|
||
"dig": ("Dig", "方位力量"),
|
||
"drik": ("Drik", "相位力量"),
|
||
"raw_total": ("Raw BAV total", "原始 BAV 合计"),
|
||
"trikona_total": ("After Trikona reduction", "三方净化后"),
|
||
"sodhita_total": ("After Ekadhipatya reduction", "单宫主净化后"),
|
||
"rashi_pinda": ("Rashi Pinda", "Rashi Pinda"),
|
||
"graha_pinda": ("Graha Pinda", "Graha Pinda"),
|
||
"shodhya_pinda": ("Shodhya Pinda", "Shodhya Pinda"),
|
||
"yoga_pinda": ("Yoga Pinda", "Yoga Pinda"),
|
||
"own_bindu": ("Bindus in own sign", "所在星座点数"),
|
||
"weight": ("Planet weight", "行星权重"),
|
||
"signs": ("Sign pair", "星座对"),
|
||
"before": ("Before", "净化前"),
|
||
"after": ("After", "净化后"),
|
||
"argala": ("Argala count", "Argala 数"),
|
||
"virodhargala": ("Virodhargala count", "Virodhargala 数"),
|
||
"effective": ("Effective Argala houses", "有效 Argala 宫"),
|
||
"priority": ("Priority", "优先级"),
|
||
"layer": ("Layer", "层"),
|
||
"target": ("Target", "目标"),
|
||
"seven_planet": ("Seven-karaka planet", "七曜口径行星"),
|
||
"seven_degree": ("Seven-karaka degree", "七曜口径度数"),
|
||
"eight_planet": ("Eight-karaka planet", "八曜口径行星"),
|
||
"eight_degree": ("Eight-karaka ranking degree", "八曜口径排序度数"),
|
||
"rahu_reversed": ("Rahu counted in reverse", "罗睺倒数"),
|
||
"tara_index": ("Tara index", "Tara 序号"),
|
||
"tara": ("Tara", "Tara"),
|
||
"tara_quality": ("Tara quality", "Tara 吉凶"),
|
||
"chandra_offset": ("Chandra place", "Chandra 位"),
|
||
"chandra": ("Chandra", "Chandra"),
|
||
"chandra_quality": ("Chandra quality", "Chandra 吉凶"),
|
||
"combined": ("Combined", "合并结果"),
|
||
"quality": ("Quality", "吉凶"),
|
||
"residual": ("Residual degrees", "剩余度数"),
|
||
}
|
||
|
||
CHAPTERS: tuple[tuple[str, str], ...] = (
|
||
("Dasha Appendix", "大运附表"),
|
||
("Annual Appendix", "年运附表"),
|
||
("Strength and Ashtakavarga", "力量与八分法"),
|
||
("Relationships, Aspects and Yogas", "关系、相位与瑜伽"),
|
||
("Nakshatras, Special Points and Transits", "星宿、特殊点与行运"),
|
||
("Calculation Conventions", "计算口径"),
|
||
)
|
||
|
||
SECTIONS: tuple[tuple[str, str], ...] = (
|
||
("Vimshottari Birth-Balance", "Vimshottari 出生余额"),
|
||
("Kalachakra Dasha", "Kalachakra 大运"),
|
||
("Yogini Periods", "Yogini 周期"),
|
||
("Tribhagi Profile", "Tribhagi 适用情况"),
|
||
("Tribhagi Periods", "Tribhagi 周期"),
|
||
("Navamsha AD Duration", "Navamsha 小运时长"),
|
||
("Nakshatra Dasha Context", "星宿大运上下文"),
|
||
("Bhrigu Cycle", "Bhrigu 周期"),
|
||
("Bhrigu Approximate Progression", "Bhrigu 近似推进序列"),
|
||
("Conditional Dasha Applicability", "条件大运是否适用"),
|
||
("Dasha Boundary Scan", "大运与小运交界检查"),
|
||
("Current Dasha Convergence", "当前多系统收敛"),
|
||
("Chara Dasha Period Durations", "Chara 大运时长"),
|
||
("Narayana Method Comparison", "Narayana 口径对照"),
|
||
("Annual Return House Framework", "年运整宫框架"),
|
||
("Annual Return Calculation Contract", "年运返照计算约定"),
|
||
("Tajika Aspect and Yoga Candidates", "Tajika 相位与瑜伽候选"),
|
||
("Tripataki Chakra Detail", "Tripataki 轮盘明细"),
|
||
("Annual Tajika Strength", "年度 Tajika 力量"),
|
||
("Annual Saham Formula Trace", "年度特殊点公式轨迹"),
|
||
("Shadbala Subcomponent", "六维力量分项"),
|
||
("Shadbala Result", "六维力量结果"),
|
||
("House Components and Physical Coordinates", "宫位分量与坐标"),
|
||
("Bhava Bala Three-Component", "Bhava Bala 三分量"),
|
||
("Vimsopaka Contribution", "Vimsopaka 分盘贡献"),
|
||
("AV Contribution and Reduction", "八分法加减"),
|
||
("AV Yoga Pinda", "八分法 Yoga Pinda"),
|
||
("AV Sign Assessments", "八分法星座评估"),
|
||
("Prastara Totals", "Prastara 合计"),
|
||
("Avastha Inputs", "行星状态输入"),
|
||
("D30 States", "D30 状态"),
|
||
("D30 Source Input", "D30 输入"),
|
||
("Pushkara Basis", "Pushkara 判断依据"),
|
||
("Lagna Vargottamamsa", "上升 Vargottamamsa"),
|
||
("Goel Jaimini Strength and Special Points", "Goel Jaimini 强度与特殊点"),
|
||
("Neutral and Compound Relationships", "行星中性与复合友敌关系"),
|
||
("Relationship Conventions", "关系口径"),
|
||
("Dispositor Terminals", "定位星终点与循环"),
|
||
("Dispositor Chain", "定位星链"),
|
||
("Aspect Geometry", "相位几何"),
|
||
("Argala", "Argala 支持与阻挡"),
|
||
("Congregation", "行星聚集"),
|
||
("Bhrigu Relationship Geometry", "Bhrigu 关系几何"),
|
||
("Yoga Participants", "瑜伽参与行星"),
|
||
("Canonical Yoga Rows", "瑜伽条目"),
|
||
("Whole-Sign Occupants", "整宫落入"),
|
||
("Nakshatra Reference", "星宿参照"),
|
||
("Tara-Chandra Matrix", "Tara 与 Chandra 矩阵"),
|
||
("Natal Tithi Lord", "本命 Tithi 主星"),
|
||
("Birth Calendar", "出生历法"),
|
||
("Pancha Pakshi", "五鸟活动"),
|
||
("Natal Vivah Saham", "本命 Vivah Saham"),
|
||
("Natal Saham Formula Basis", "本命特殊点公式依据"),
|
||
("Natal Saham Formula Trace", "本命特殊点公式轨迹"),
|
||
("Arudha / UL / A10", "Arudha、Upapada 与 A10"),
|
||
("Jaimini Calculation Details", "Jaimini 计算细节"),
|
||
("Graha Pada", "Graha Pada"),
|
||
("Jaimini Yogi and Ava Yogi", "Jaimini Yogi 与 Ava Yogi"),
|
||
("Karakamsha", "Karakamsha"),
|
||
("Darakaraka", "Darakaraka"),
|
||
("Rashi Tulya Navamsa", "Rashi Tulya Navamsa"),
|
||
("Moon Chart Reference", "月亮参考盘"),
|
||
("Sudarshana Points", "Sudarshana 参考点"),
|
||
("Dated Transit", "注明日期的行运"),
|
||
("Multi-Reference Transit", "多参考点行运"),
|
||
("Double Transit PAC", "木土双重行运 PAC"),
|
||
("Lagna / 7th Lord Transit", "上升主与七宫主行运"),
|
||
("Natal Calculation Conventions", "本命计算口径"),
|
||
("KP Calculation Conventions", "KP 计算口径"),
|
||
("D9 Dignity", "D9 尊贵"),
|
||
("Divisional Mapping", "分盘映射"),
|
||
("Bhava Chalit Planet Placements", "Bhava Chalit 行星落宫"),
|
||
("Bhava Chalit Centres", "Bhava Chalit 宫心"),
|
||
("Bhava Chalit Convention", "Bhava Chalit 口径"),
|
||
)
|
||
|
||
FORBIDDEN_TITLES = (
|
||
"Annual Chart Numeric Data",
|
||
"Bhava Bala Relative Score Components",
|
||
"Score Extremes",
|
||
"独立年度方法参考",
|
||
"三年逐月星历",
|
||
"D81",
|
||
"D108",
|
||
"D144",
|
||
)
|
||
|
||
_SNAKE = re.compile(r"^[a-z0-9_]+$")
|
||
_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}")
|
||
|
||
ENUMS: dict[str, tuple[str, str]] = {
|
||
"blocked": ("Not determined", "未判定"),
|
||
"executed": ("Computed", "已算出"),
|
||
"computed": ("Computed", "已算出"),
|
||
"partial_verified": ("Partly computed", "部分已算出"),
|
||
"not_applicable": ("Not applicable", "不适用"),
|
||
"selected": ("Selected", "已选定"),
|
||
"conjunction": ("Conjunction", "合相"),
|
||
"opposition": ("Opposition", "对冲"),
|
||
"trine": ("Trine", "拱相"),
|
||
"square": ("Square", "刑相"),
|
||
"sextile": ("Sextile", "六合"),
|
||
"exalted": ("Exalted", "擢升"),
|
||
"debilitated": ("Debilitated", "落陷"),
|
||
"mooltrikona": ("Moolatrikona", "根本三分"),
|
||
"own": ("Own sign", "入庙"),
|
||
"include_nodes_text": ("Include nodes (text profile)", "计入交点(正文口径)"),
|
||
"exclude_nodes_worked_example": ("Worked example excludes nodes", "例题不计交点"),
|
||
"sripati": ("Sripati", "Sripati"),
|
||
"sandhi": ("Sandhi", "交界"),
|
||
"full": ("Full", "完全"),
|
||
"partial": ("Partial", "部分"),
|
||
"none": ("None", "无"),
|
||
"start": ("Start", "开始"),
|
||
"end": ("End", "结束"),
|
||
"mahadasha": ("Mahadasha", "大运"),
|
||
"antardasha": ("Antardasha", "小运"),
|
||
"applicable": ("Applicable", "适用"),
|
||
"mean": ("Mean node", "平交点"),
|
||
"true": ("True node", "真交点"),
|
||
"parameter_sensitive": ("Parameter-sensitive", "随参数变化"),
|
||
"friend": ("Friend", "友"),
|
||
"enemy": ("Enemy", "敌"),
|
||
"neutral": ("Neutral", "中性"),
|
||
"great_friend": ("Great friend", "极友"),
|
||
"great_enemy": ("Great enemy", "极敌"),
|
||
"applying": ("Applying", "入相位"),
|
||
"separating": ("Separating", "离相位"),
|
||
"Ithasala_candidate": ("Ithasala candidate", "Ithasala 候选"),
|
||
"Easarapha_candidate": ("Easarapha candidate", "Easarapha 候选"),
|
||
}
|
||
|
||
PROFILES: dict[str, tuple[str, str]] = {
|
||
"legacy_forward_v0": ("Legacy duration", "既有时长"),
|
||
"legacy_zodiacal_v0": ("Legacy sequence", "既有顺序"),
|
||
"jaimini_odd_footed_dignity_v2": ("Odd-footed dignity duration", "奇足尊贵时长"),
|
||
"rath_general_table10_v1": ("Rath table 10", "Rath 表10"),
|
||
"rath_saturn_table11_v1": ("Rath table 11", "Rath 表11"),
|
||
"jaimini_lagna_seventh_strength_v1": ("Lagna and seventh strength", "上升与七宫力量"),
|
||
"parashara_equal_v1": ("Equal sub-periods", "等分小运"),
|
||
"legacy_primary_lord_v0": ("Legacy primary lord", "既有主星"),
|
||
"legacy_lagna_v0": ("From the ascendant", "从上升起"),
|
||
"legacy_comparison_only": ("Legacy comparison only", "只保留既有对照"),
|
||
}
|
||
|
||
REASONS: dict[str, tuple[str, str]] = {
|
||
"rath_ketu_table12_v1_not_in_local_sequence_profiles": (
|
||
"No local table-12 sequence when Ketu is in the seed sign",
|
||
"计都在起算星座时,本地没有表12顺序",
|
||
),
|
||
"eighth_lord_from_AK_occupies_eighth_from_AK": (
|
||
"The eighth lord from the atmakaraka occupies that eighth sign",
|
||
"灵魂指示星起第八宫的宫主落在该宫",
|
||
),
|
||
"strongest_6_8_12_lord_from_stronger_Lagna_seventh_excluding_Saturn": (
|
||
"Strongest of the 6th, 8th and 12th lords from the stronger of lagna and the seventh, excluding Saturn",
|
||
"上升与第七宫中较强的一方,取其 6、8、12 宫主里最强且不是土星的一颗",
|
||
),
|
||
"eighth_lord_from_AK_compare_twelfth_if_exalted": (
|
||
"Eighth lord from the atmakaraka, compared with the twelfth lord when that eighth lord is exalted",
|
||
"灵魂指示星起第八宫主;若该星擢升,再与第十二宫主比较",
|
||
),
|
||
"stronger_8th_2nd_lord_unless_only_weaker_has_malefic_contact": (
|
||
"The stronger of the 8th and 2nd lords, unless only the weaker one has malefic contact",
|
||
"第八宫主与第二宫主取较强者;仅较弱者受凶星接触时改取较弱者",
|
||
),
|
||
"equal_Goel_strength_no_unique_selector": (
|
||
"Equal strength, so there is no single selected planet",
|
||
"力量相同,没有唯一入选星",
|
||
),
|
||
"equal_Lagna_seventh_Goel_strength": (
|
||
"Lagna and the seventh have equal strength",
|
||
"上升与第七宫力量相同",
|
||
),
|
||
"equal_degree_seven_karaka_rank_requires_finer_positions": (
|
||
"Equal degrees among the seven planets; the present precision cannot rank them",
|
||
"七颗星度数相同,现有精度排不出顺序",
|
||
),
|
||
}
|
||
|
||
DOMAINS: dict[str, tuple[str, str]] = {
|
||
"self_health": ("Self and health", "自身与健康"),
|
||
"wealth_family": ("Resources and family", "资源与家庭"),
|
||
"communication_skill": ("Communication and skill", "沟通与技能"),
|
||
"home_mother": ("Home and mother", "家宅与母亲"),
|
||
"creativity_children": ("Creativity and children", "创造与子女"),
|
||
"health_service": ("Health and service", "健康与事务"),
|
||
"marriage_partnership": ("Marriage and partnership", "婚姻与合作"),
|
||
"transformation": ("Transformation", "转变"),
|
||
"fortune_dharma": ("Fortune and dharma", "福运与法"),
|
||
"career_status": ("Career and status", "事业与地位"),
|
||
"gains_wishes": ("Gains and wishes", "收获与愿望"),
|
||
"loss_spirituality": ("Loss and inward life", "损耗与内在"),
|
||
}
|
||
|
||
LEVELS: dict[str, tuple[str, str]] = {
|
||
"L1": ("One system is active; transits still have to be read with it.", "单系统激活,还要看行运。"),
|
||
"L3": ("Two systems are active together.", "两个系统同时激活。"),
|
||
"L4": ("Three systems are active together.", "三个系统同时激活。"),
|
||
"L5": ("Four or more systems are active together.", "四个及以上系统同时激活。"),
|
||
}
|
||
|
||
FAMILIES: dict[str, tuple[str, str]] = {
|
||
"ashtottari": ("Ashtottari", "Ashtottari"),
|
||
"tribhagi": ("Tribhagi", "Tribhagi"),
|
||
"tribhagi_40": ("Tribhagi 40", "Tribhagi 40"),
|
||
"shodashottari": ("Shodashottari", "Shodashottari"),
|
||
"dwadashottari": ("Dwadashottari", "Dwadashottari"),
|
||
"dwisaptatisama": ("Dwisaptati Sama", "Dwisaptati Sama"),
|
||
"shattrimshatsama": ("Shattrimsha Sama", "Shattrimsha Sama"),
|
||
"panchottari": ("Panchottari", "Panchottari"),
|
||
"satabdika": ("Satabdika", "Satabdika"),
|
||
"chaturshitisama": ("Chaturshiti Sama", "Chaturshiti Sama"),
|
||
"shastihayani": ("Shastihayani", "六十年运"),
|
||
"kala_chakra": ("Kalachakra", "Kalachakra"),
|
||
"yogini": ("Yogini", "Yogini"),
|
||
"narayana": ("Narayana", "Narayana"),
|
||
}
|
||
|
||
ASPECT_TYPES = {
|
||
"conjunction": ("Conjunction", "合相"),
|
||
"opposition": ("Opposition", "对冲"),
|
||
"square": ("Square", "刑相"),
|
||
"trine": ("Trine", "拱相"),
|
||
"sextile": ("Sextile", "六合"),
|
||
}
|
||
|
||
|
||
def lab(key: str, language: str) -> str:
|
||
pair = LABELS.get(key)
|
||
if not pair or not pair[0] or not pair[1]:
|
||
raise KeyError(key)
|
||
return pair[1] if language == "zh" else pair[0]
|
||
|
||
|
||
def _enum(value: Any, language: str) -> str | None:
|
||
text = str(value or "")
|
||
for table in (ENUMS, PROFILES, REASONS, DOMAINS, FAMILIES):
|
||
pair = table.get(text)
|
||
if pair:
|
||
return pair[1] if language == "zh" else pair[0]
|
||
level = LEVELS.get(text)
|
||
if level:
|
||
return level[1] if language == "zh" else level[0]
|
||
return None
|
||
|
||
|
||
def show(value: Any, language: str, kind: str = "text") -> str:
|
||
if isinstance(value, dict):
|
||
return undetermined(language)
|
||
if value is None or value == "":
|
||
return undetermined(language)
|
||
if isinstance(value, bool):
|
||
return yes_no(value, language)
|
||
if isinstance(value, float):
|
||
return f"{value:.4f}".rstrip("0").rstrip(".")
|
||
if isinstance(value, int) and kind == "sign":
|
||
named = sign_name(value, language)
|
||
return _english_safe(named, language)
|
||
if isinstance(value, int):
|
||
return str(value)
|
||
if kind == "planet":
|
||
named = planet_name(value, language)
|
||
if "_" in str(named):
|
||
return undetermined(language)
|
||
return _english_safe(named, language)
|
||
if kind == "sign":
|
||
return _english_safe(sign_name(value, language), language)
|
||
if kind == "planets":
|
||
return _english_safe(planet_list(value, language) or undetermined(language), language)
|
||
if isinstance(value, (list, tuple)):
|
||
parts = [show(item, language, "planet" if kind == "planet_list" else "text") for item in value[:12]]
|
||
parts = [part for part in parts if part and part != undetermined(language)]
|
||
if not parts:
|
||
return undetermined(language)
|
||
joined = "、".join(parts) if language == "zh" else ", ".join(parts)
|
||
return _english_safe(joined, language)
|
||
text = str(value).strip()
|
||
mapped = _enum(text, language)
|
||
if mapped:
|
||
return mapped
|
||
if _SNAKE.fullmatch(text) and re.search(r"[a-z]", text):
|
||
# A bare lower-case token is an internal key or enum, not reader text.
|
||
return undetermined(language)
|
||
if re.search(r"\b(?:source_locator|profile_id|niryaana_shoola_strength|source_discrepancy|parameter_sensitive|sign_idx)\b", text):
|
||
return undetermined(language)
|
||
if re.search(r"[A-Za-z0-9]+_[A-Za-z0-9_]+", text):
|
||
return undetermined(language)
|
||
if language == "en" and re.search(r"[\u4e00-\u9fff]", text):
|
||
return undetermined(language)
|
||
if language == "zh" and re.search(r"\b(in|and|or|from|planet|lord)\b", text, re.IGNORECASE):
|
||
return sign_name(text, language) if text in SIGNS else undetermined(language)
|
||
if kind == "sign" or text in SIGNS:
|
||
return _english_safe(sign_name(text, language), language)
|
||
return text
|
||
|
||
|
||
def _english_safe(text: str, language: str) -> str:
|
||
if language == "en" and re.search(r"[\u4e00-\u9fff]", text or ""):
|
||
return undetermined(language)
|
||
return text or undetermined(language)
|
||
|
||
|
||
def _dig(root: Any, path: tuple[str, ...]) -> Any:
|
||
current = root
|
||
for key in path:
|
||
if not isinstance(current, dict) or key not in current:
|
||
return None
|
||
current = current[key]
|
||
return current
|
||
|
||
|
||
def _first(packet: dict, tail: tuple[str, ...]) -> Any:
|
||
for prefix in ((), ("worksheets",)):
|
||
found = _dig(packet, prefix + tail)
|
||
if found not in (None, {}, ""):
|
||
return found
|
||
return None
|
||
|
||
|
||
def _md_table(headers: list[str], rows: list[list[Any]]) -> str:
|
||
if not rows:
|
||
return ""
|
||
lines = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"] * len(headers)) + "|"]
|
||
for row in rows:
|
||
cells = [str(cell).replace("|", "/").replace("\n", " ") for cell in row]
|
||
lines.append("| " + " | ".join(cells) + " |")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _section(title_en: str, language: str, body: str) -> str:
|
||
titles = dict(SECTIONS)
|
||
zh = titles[title_en]
|
||
heading = zh if language == "zh" else title_en
|
||
text = body.strip() or undetermined(language)
|
||
return f"### {heading}\n\n{text}"
|
||
|
||
|
||
def _blank(language: str, columns: list[str]) -> str:
|
||
headers = [lab(key, language) for key in columns]
|
||
row = [undetermined(language) for _ in columns]
|
||
return _md_table(headers, [row])
|
||
|
||
|
||
def _rows_from(items: list[Any], language: str, columns: list[tuple[str, str]]) -> str:
|
||
headers = [lab(key, language) for key, _kind in columns]
|
||
rows: list[list[str]] = []
|
||
for item in items:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append([show(item.get(key), language, kind) for key, kind in columns])
|
||
if not rows:
|
||
return _blank(language, [key for key, _kind in columns])
|
||
keep = [index for index, _column in enumerate(columns) if any(row[index] != undetermined(language) for row in rows)]
|
||
if not keep:
|
||
return _blank(language, [key for key, _kind in columns])
|
||
headers = [headers[index] for index in keep]
|
||
rows = [[row[index] for index in keep] for row in rows]
|
||
return _md_table(headers, rows)
|
||
|
||
|
||
_PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
||
_MISSING_MODULE = {
|
||
"zh": "公开报告资料包未带此模块",
|
||
"en": "This module is not in the public report packet",
|
||
}
|
||
|
||
|
||
def _missing_module(language: str, columns: list[str]) -> str:
|
||
"""The public worksheet does not carry this module. Keep the undetermined row."""
|
||
return _MISSING_MODULE[language] + "\n\n" + _blank(language, columns)
|
||
|
||
|
||
def _is_planet(value: Any) -> bool:
|
||
return isinstance(value, str) and value.strip() in _PLANETS
|
||
|
||
|
||
def _dignity_token(value: Any, language: str) -> Any:
|
||
if not isinstance(value, str):
|
||
return value
|
||
key = " ".join(value.strip().split()).lower()
|
||
if key == "neutral":
|
||
return "中性" if language == "zh" else "Neutral"
|
||
return {
|
||
"exalted": "exalted",
|
||
"debilitated": "debilitated",
|
||
"own sign": "own",
|
||
"own": "own",
|
||
"moolatrikona": "mooltrikona",
|
||
}.get(key, value)
|
||
|
||
|
||
def _named_planets(chart: Any) -> list[tuple[str, dict]]:
|
||
if not isinstance(chart, dict):
|
||
return []
|
||
nested = chart.get("planets")
|
||
source = nested if isinstance(nested, dict) else chart
|
||
rows = []
|
||
for name in _PLANETS:
|
||
item = source.get(name)
|
||
if isinstance(item, dict):
|
||
rows.append((name, item))
|
||
return rows
|
||
|
||
|
||
def _varga_chart(packet: dict, *keys: str) -> dict:
|
||
varga = _first(packet, ("divisional_and_special_charts", "varga_full")) or {}
|
||
if not isinstance(varga, dict):
|
||
return {}
|
||
for key in keys:
|
||
chart = varga.get(key)
|
||
if isinstance(chart, dict):
|
||
return chart
|
||
return {}
|
||
|
||
|
||
def _varga_label(name: str, chart: dict) -> str:
|
||
meta = chart.get("_meta") if isinstance(chart.get("_meta"), dict) else {}
|
||
div = meta.get("div")
|
||
title = str(meta.get("name") or "")
|
||
if div and title and "_" not in title and not re.search(r"[\u4e00-\u9fff]", title):
|
||
return f"D{div} {title}".strip()
|
||
return " ".join(str(name).replace("_", " ").split())
|
||
|
||
|
||
def _cycle_label(value: str, language: str) -> str | None:
|
||
body = value.split(":", 1)[1]
|
||
parts = [part.strip(" .") for part in body.replace("->", "|").replace("→", "|").split("|")]
|
||
names = []
|
||
for part in parts:
|
||
if not _is_planet(part):
|
||
return None
|
||
names.append(planet_name(part, language))
|
||
if len(names) < 2:
|
||
return None
|
||
prefix = "循环" if language == "zh" else "Cycle"
|
||
return prefix + " " + " → ".join(names)
|
||
|
||
|
||
def _months(start: Any, end: Any) -> str | None:
|
||
if not isinstance(start, str) or not isinstance(end, str):
|
||
return None
|
||
if not _DATE.match(start) or not _DATE.match(end):
|
||
return None
|
||
from datetime import datetime
|
||
try:
|
||
days = (datetime.strptime(end[:10], "%Y-%m-%d") - datetime.strptime(start[:10], "%Y-%m-%d")).days
|
||
except ValueError:
|
||
return None
|
||
if days <= 0:
|
||
return None
|
||
return f"{days / 30.436875:.2f}".rstrip("0").rstrip(".")
|
||
|
||
|
||
def _vimshottari(packet: dict, language: str) -> str:
|
||
dasha = _first(packet, ("timing_and_predictive_systems", "dasha"))
|
||
timeline = dasha.get("timeline") if isinstance(dasha, dict) else None
|
||
row = next((item for item in timeline or [] if isinstance(item, dict) and item.get("is_balance")), None)
|
||
if not isinstance(row, dict):
|
||
return _blank(language, ["lord", "years", "start", "end"])
|
||
return _rows_from([row], language, [
|
||
("lord", "planet"), ("years", "text"), ("full_years", "text"),
|
||
("balance_years", "text"), ("elapsed_at_birth", "text"),
|
||
("start", "text"), ("end", "text"),
|
||
])
|
||
|
||
|
||
def _major_list(module: Any) -> list[Any]:
|
||
if not isinstance(module, dict):
|
||
return []
|
||
for key in ("major", "mahadasha", "periods", "dasha_sequence"):
|
||
value = module.get(key)
|
||
if isinstance(value, list) and value:
|
||
return value
|
||
return []
|
||
|
||
|
||
def _kalachakra(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "kalachakra_dasha"))
|
||
rows = [item for item in _major_list(module) if isinstance(item, dict) and "antardasha" not in item and "subperiods" not in item]
|
||
if not rows:
|
||
rows = [item for item in _major_list(module) if isinstance(item, dict)]
|
||
return _rows_from(rows, language, [
|
||
("lord", "planet"), ("rashi", "sign"), ("sign", "sign"),
|
||
("years", "text"), ("start_date", "text"), ("end_date", "text"),
|
||
("start", "text"), ("end", "text"),
|
||
])
|
||
|
||
|
||
def _yogini(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "yogini_dasha"))
|
||
return _rows_from(_major_list(module), language, [
|
||
("yogini", "text"), ("lord", "planet"), ("planet", "planet"),
|
||
("years", "text"), ("start", "text"), ("end", "text"),
|
||
("start_date", "text"), ("end_date", "text"),
|
||
])
|
||
|
||
|
||
def _family(packet: dict, key: str) -> dict:
|
||
families = _first(packet, ("timing_and_predictive_systems", "dasha_master_pack", "families"))
|
||
item = families.get(key) if isinstance(families, dict) else None
|
||
return item if isinstance(item, dict) else {}
|
||
|
||
|
||
def _tribhagi_profile(packet: dict, language: str) -> str:
|
||
rows = []
|
||
for key in ("tribhagi", "tribhagi_40"):
|
||
item = _family(packet, key)
|
||
status = str(item.get("execution_status") or item.get("status") or "")
|
||
applicable = "applicable" if status == "executed" else "not_applicable" if status == "not_applicable" else ""
|
||
rows.append({
|
||
"name": FAMILIES[key][1 if language == "zh" else 0],
|
||
"applicable": applicable or None,
|
||
})
|
||
return _rows_from(rows, language, [("name", "text"), ("applicable", "text")])
|
||
|
||
|
||
def _tribhagi_periods(packet: dict, language: str) -> str:
|
||
periods = _family(packet, "tribhagi").get("periods")
|
||
if not isinstance(periods, list):
|
||
periods = []
|
||
return _rows_from(periods, language, [
|
||
("lord", "planet"), ("planet", "planet"), ("sign", "sign"),
|
||
("years", "text"), ("start_date", "text"), ("end_date", "text"),
|
||
("start", "text"), ("end", "text"),
|
||
])
|
||
|
||
|
||
def _navamsha(packet: dict, language: str) -> str:
|
||
del packet
|
||
return _missing_module(language, ["sign", "start", "end", "months"])
|
||
|
||
|
||
def _nakshatra_context(packet: dict, language: str) -> str:
|
||
dasha = _first(packet, ("timing_and_predictive_systems", "dasha")) or {}
|
||
ashtottari = _first(packet, ("timing_and_predictive_systems", "ashtottari_dasha")) or {}
|
||
row = {
|
||
"nakshatra": dasha.get("moon_nakshatra") or dasha.get("nakshatra") if isinstance(dasha, dict) else None,
|
||
"lord": dasha.get("start_lord") if isinstance(dasha, dict) else None,
|
||
"applicable": ashtottari.get("applicable") if isinstance(ashtottari, dict) else None,
|
||
}
|
||
return _rows_from([row], language, [("nakshatra", "text"), ("lord", "planet"), ("applicable", "text")])
|
||
|
||
|
||
def _bhrigu_cycle(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "bhrigu_pada_dasha")) or {}
|
||
moon = module.get("birth_moon") if isinstance(module, dict) else {}
|
||
if not isinstance(moon, dict):
|
||
moon = {}
|
||
sign = moon.get("sign_cn") if language == "zh" and moon.get("sign_cn") else moon.get("sign")
|
||
note = (
|
||
"下面的年龄是推进坐标的取样,不是事件日期。"
|
||
if language == "zh"
|
||
else "The ages below are samples of the progressed coordinate, not event dates."
|
||
)
|
||
row = {"sign": sign, "longitude": moon.get("longitude"), "method": "Bhrigu Pada"}
|
||
return note + "\n\n" + _rows_from([row], language, [("sign", "sign"), ("longitude", "text"), ("method", "text")])
|
||
|
||
|
||
def _bhrigu_sequence(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "bhrigu_pada_dasha")) or {}
|
||
rows = []
|
||
for item in module.get("dasha_sequence") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
copied = dict(item)
|
||
if language == "zh" and item.get("progressed_sign_cn"):
|
||
copied["progressed_sign"] = item.get("progressed_sign_cn")
|
||
rows.append(copied)
|
||
return _rows_from(rows, language, [
|
||
("age", "text"), ("years_elapsed", "text"), ("progressed_longitude", "text"),
|
||
("progressed_sign", "sign"), ("progressed_nakshatra", "text"),
|
||
("progressed_nakshatra_lord", "planet"), ("progressed_nakshatra_pada", "text"),
|
||
("progression_rate", "text"),
|
||
])
|
||
|
||
|
||
def _applicability(packet: dict, language: str) -> str:
|
||
rows = []
|
||
for key, pair in FAMILIES.items():
|
||
if key in {"yogini", "narayana", "kala_chakra"}:
|
||
continue
|
||
item = _family(packet, key)
|
||
if not item and key != "ashtottari":
|
||
module = _first(packet, ("timing_and_predictive_systems", f"{key}_dasha"))
|
||
item = module if isinstance(module, dict) else {}
|
||
if key == "ashtottari" and not item:
|
||
item = _first(packet, ("timing_and_predictive_systems", "ashtottari_dasha")) or {}
|
||
status = str(item.get("execution_status") or item.get("status") or "")
|
||
if item.get("applicable") is True:
|
||
applicable = "applicable"
|
||
elif item.get("applicable") is False or status == "not_applicable":
|
||
applicable = "not_applicable"
|
||
elif status == "executed":
|
||
applicable = "applicable"
|
||
else:
|
||
applicable = ""
|
||
rows.append({"name": pair[1 if language == "zh" else 0], "applicable": applicable or None})
|
||
return _rows_from(rows, language, [("name", "text"), ("applicable", "text")])
|
||
|
||
|
||
def _sandhi(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "dasha_sandhi"))
|
||
if not isinstance(module, dict):
|
||
return _blank(language, ["reference", "orb", "status"])
|
||
windows = module.get("sandhi_windows") if isinstance(module.get("sandhi_windows"), list) else []
|
||
if not windows:
|
||
sentence = (
|
||
f"参考日 {module.get('reference_date') or undetermined(language)} 起 {module.get('orb_days') if module.get('orb_days') is not None else undetermined(language)} 天内没有大运或小运交界。"
|
||
if language == "zh"
|
||
else f"No mahadasha or antardasha boundary falls within {module.get('orb_days')} days of {module.get('reference_date')}."
|
||
)
|
||
return sentence
|
||
rows = []
|
||
for item in windows:
|
||
if isinstance(item, dict):
|
||
rows.append({
|
||
"level": item.get("level"),
|
||
"lord": item.get("lord"),
|
||
"boundary": item.get("boundary"),
|
||
"date": item.get("date"),
|
||
"days": item.get("days_from_reference"),
|
||
})
|
||
return _rows_from(rows, language, [("level", "text"), ("lord", "planet"), ("boundary", "text"), ("date", "text"), ("days", "text")])
|
||
|
||
|
||
def _convergence(packet: dict, language: str) -> str:
|
||
del packet
|
||
return _missing_module(language, ["domain", "level"])
|
||
|
||
|
||
def _chara(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "jaimini", "chara_dasha")) or {}
|
||
sequence = module.get("dasha_sequence") if isinstance(module, dict) else None
|
||
rows = []
|
||
for major in sequence or []:
|
||
if not isinstance(major, dict):
|
||
continue
|
||
rows.append({"level": "mahadasha", "sign": major.get("sign"), "lord": major.get("lord"), "years": major.get("duration_years"), "order": major.get("order")})
|
||
for antar in major.get("antardashas") or []:
|
||
if not isinstance(antar, dict):
|
||
continue
|
||
rows.append({"level": "antardasha", "sign": antar.get("sign"), "lord": antar.get("lord"), "years": antar.get("duration_years"), "order": antar.get("order")})
|
||
for sub in antar.get("pratyantar_dashas") or []:
|
||
if isinstance(sub, dict):
|
||
rows.append({"level": "PD", "sign": sub.get("sign"), "lord": sub.get("lord"), "years": sub.get("duration_years"), "order": sub.get("order")})
|
||
if not rows:
|
||
return _blank(language, ["level", "sign", "lord", "years"])
|
||
rendered = []
|
||
for row in rows:
|
||
level = row["level"]
|
||
if level == "PD":
|
||
level_text = "再小运" if language == "zh" else "Pratyantardasha"
|
||
else:
|
||
level_text = show(level, language)
|
||
rendered.append([level_text, show(row["sign"], language, "sign"), show(row["lord"], language, "planet"), show(row["years"], language), show(row["order"], language)])
|
||
return _md_table([lab(key, language) for key in ("level", "sign", "lord", "years", "order")], rendered)
|
||
|
||
|
||
def _narayana_block(report: Any, language: str, fallback: str) -> str:
|
||
if not isinstance(report, dict):
|
||
return fallback
|
||
if report.get("status") == "blocked" or report.get("reason"):
|
||
reason = show(report.get("reason") or report.get("status"), language)
|
||
status = show(report.get("status") or "blocked", language)
|
||
return _rows_from([{"status": status, "value": reason}], language, [("status", "text"), ("value", "text")])
|
||
profiles = []
|
||
for key in ("duration_profile", "sequence_profile", "seed_profile", "dual_lord_profile", "antardasha_profile"):
|
||
if report.get(key):
|
||
profiles.append(show(report.get(key), language))
|
||
lines = [_rows_from([{"method": ";".join(profiles) if language == "zh" else "; ".join(profiles), "years": report.get("total_cycle_years")}], language, [("method", "text"), ("years", "text")])]
|
||
lines.append(_rows_from(report.get("mahadasha_sequence") or [], language, [
|
||
("sign", "sign"), ("lord", "planet"), ("years", "text"), ("start", "text"), ("end", "text"),
|
||
]))
|
||
return "\n\n".join(lines)
|
||
|
||
|
||
def _narayana(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "narayana_dasha")) or {}
|
||
comparison = module.get("method_comparison") if isinstance(module, dict) else {}
|
||
if not isinstance(comparison, dict):
|
||
comparison = {}
|
||
source = comparison.get("source_aware")
|
||
legacy = comparison.get("legacy") or module
|
||
source_title = "来源感知口径" if language == "zh" else "Source-aware method"
|
||
legacy_title = "既有口径" if language == "zh" else "Legacy method"
|
||
missing = undetermined(language)
|
||
return "\n\n".join([
|
||
f"#### {source_title}",
|
||
"",
|
||
_narayana_block(source, language, missing),
|
||
"",
|
||
f"#### {legacy_title}",
|
||
"",
|
||
_narayana_block(legacy, language, missing),
|
||
])
|
||
|
||
|
||
def _pack_data(field: Any) -> Any:
|
||
if isinstance(field, dict) and "data" in field and isinstance(field.get("data"), (dict, list)):
|
||
return field["data"]
|
||
return field
|
||
|
||
|
||
def _occupants_cell(names: Any, language: str) -> str:
|
||
if isinstance(names, list) and names:
|
||
return planet_list(names, language) or ("无" if language == "zh" else "None")
|
||
if isinstance(names, list):
|
||
return "无" if language == "zh" else "None"
|
||
return names if isinstance(names, str) else ("无" if language == "zh" else "None")
|
||
|
||
|
||
def _phase_word(phase: Any, language: str) -> str | None:
|
||
if phase == "day":
|
||
return "日间" if language == "zh" else "Day"
|
||
if phase == "night":
|
||
return "夜间" if language == "zh" else "Night"
|
||
return None
|
||
|
||
|
||
def _annual_houses(packet: dict, language: str) -> str:
|
||
pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
|
||
data = _pack_data(pack.get("annual_houses") if isinstance(pack, dict) else None)
|
||
houses = data.get("houses") if isinstance(data, dict) else None
|
||
rows = []
|
||
for item in houses or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"house": item.get("house"),
|
||
"sign": item.get("sign"),
|
||
"lord": item.get("lord"),
|
||
"occupant": _occupants_cell(item.get("occupants"), language),
|
||
})
|
||
return _rows_from(rows, language, [("house", "text"), ("sign", "sign"), ("lord", "planet"), ("occupant", "text")])
|
||
|
||
|
||
def _tripataki(packet: dict, language: str) -> str:
|
||
pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
|
||
data = _pack_data(pack.get("tripataki_chakra") if isinstance(pack, dict) else None)
|
||
if not isinstance(data, dict):
|
||
return _blank(language, ["point", "sign", "degree", "longitude"])
|
||
placements = []
|
||
for item in data.get("placements") or []:
|
||
if isinstance(item, dict):
|
||
placements.append({
|
||
"point": item.get("body"),
|
||
"sign": item.get("sign"),
|
||
"degree": None if item.get("degree_in_sign") is None else round(float(item["degree_in_sign"]), 4),
|
||
"longitude": None if item.get("longitude") is None else round(float(item["longitude"]), 4),
|
||
})
|
||
vedha = []
|
||
for item in data.get("vedha_rows") or []:
|
||
if isinstance(item, dict):
|
||
vedha.append({
|
||
"target": item.get("target"),
|
||
"planet": item.get("available_sources") or item.get("sources"),
|
||
"status": item.get("status"),
|
||
})
|
||
parts = [
|
||
_rows_from(placements, language, [("point", "text"), ("sign", "sign"), ("degree", "text"), ("longitude", "text")]),
|
||
_rows_from(vedha, language, [("target", "planet"), ("planet", "planets"), ("status", "text")]),
|
||
]
|
||
return "\n\n".join(parts)
|
||
|
||
|
||
def _trace_table(traces: Any, language: str) -> str:
|
||
rows = []
|
||
if isinstance(traces, list):
|
||
for item in traces:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"name": item.get("name"),
|
||
"rule": item.get("rule"),
|
||
"phase": _phase_word(item.get("phase"), language),
|
||
"longitude": item.get("longitude"),
|
||
})
|
||
return _rows_from(rows, language, [("name", "text"), ("rule", "text"), ("phase", "text"), ("longitude", "text")])
|
||
|
||
|
||
def _annual_saham_trace(packet: dict, language: str) -> str:
|
||
pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
|
||
sahams = _pack_data(pack.get("sahams") if isinstance(pack, dict) else None)
|
||
traces = sahams.get("formula_traces") if isinstance(sahams, dict) else None
|
||
return _trace_table(traces, language)
|
||
|
||
|
||
def _natal_saham_trace(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "sahams")) or {}
|
||
traces = module.get("formula_traces") if isinstance(module, dict) else None
|
||
return _trace_table(traces, language)
|
||
|
||
|
||
def _annual(packet: dict, language: str) -> str:
|
||
pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
|
||
solar = pack.get("solar_return") if isinstance(pack, dict) else {}
|
||
chart = pack.get("annual_chart") if isinstance(pack, dict) else {}
|
||
muntha = pack.get("muntha") if isinstance(pack, dict) else {}
|
||
year_lord = pack.get("year_lord") if isinstance(pack, dict) else {}
|
||
solar_data = solar.get("data") if isinstance(solar, dict) else {}
|
||
chart_data = chart.get("data") if isinstance(chart, dict) else {}
|
||
muntha_data = muntha.get("data") if isinstance(muntha, dict) else {}
|
||
lord_data = year_lord.get("data") if isinstance(year_lord, dict) else {}
|
||
row = {
|
||
"date": (solar_data or {}).get("dt_local"),
|
||
"sign": (chart_data or {}).get("asc_sign"),
|
||
"value": (muntha_data or {}).get("muntha_sign"),
|
||
"lord": (lord_data or {}).get("year_lord"),
|
||
}
|
||
return _rows_from([row], language, [("date", "text"), ("sign", "sign"), ("value", "sign"), ("lord", "planet")])
|
||
|
||
|
||
def _tajika_candidates(packet: dict, language: str) -> str:
|
||
pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
|
||
field = pack.get("tajika_yogas") if isinstance(pack, dict) else {}
|
||
data = field.get("data") if isinstance(field, dict) else {}
|
||
rows = []
|
||
for item in (data or {}).get("candidate_yogas") or (data or {}).get("interactions") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"name": item.get("name") or item.get("yoga") or item.get("type"),
|
||
"planet": item.get("planet") or item.get("planets") or item.get("forming_planets"),
|
||
"aspect": item.get("aspect"),
|
||
"residual": item.get("residual"),
|
||
"status": item.get("present") if "present" in item else item.get("motion") or item.get("status"),
|
||
})
|
||
if not rows:
|
||
return "本年没有候选" if language == "zh" else "No candidates this year"
|
||
return _rows_from(rows, language, [("name", "text"), ("planet", "planets"), ("aspect", "text"), ("residual", "text"), ("status", "text")])
|
||
|
||
|
||
def _tajika_strength(packet: dict, language: str) -> str:
|
||
pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {}
|
||
field = pack.get("tajika_strength") if isinstance(pack, dict) else {}
|
||
data = field.get("data") if isinstance(field, dict) else {}
|
||
rows = []
|
||
for label, key in (("Harsha", "harsha_bala"), ("Panchavargiya", "panchavargiya_bala")):
|
||
table = (data or {}).get(key) if isinstance(data, dict) else None
|
||
if not isinstance(table, dict):
|
||
continue
|
||
for planet, item in table.items():
|
||
score = item.get("score") if isinstance(item, dict) else item
|
||
rows.append({"name": label, "planet": planet, "score": score})
|
||
return _rows_from(rows, language, [("name", "text"), ("planet", "planet"), ("score", "text")])
|
||
|
||
|
||
def _shadbala_planets(packet: dict) -> dict:
|
||
module = _first(packet, ("strengths_and_scores", "shadbala")) or {}
|
||
planets = module.get("planets") if isinstance(module, dict) else None
|
||
return planets if isinstance(planets, dict) else {}
|
||
|
||
|
||
def _component_number(value: Any) -> Any:
|
||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||
return value
|
||
if isinstance(value, dict):
|
||
for key in ("total", "score", "value", "virupas"):
|
||
if isinstance(value.get(key), (int, float)) and not isinstance(value.get(key), bool):
|
||
return value[key]
|
||
return None
|
||
|
||
|
||
def _shadbala_parts(packet: dict, language: str, result_only: bool) -> str:
|
||
rows = []
|
||
for planet, item in _shadbala_planets(packet).items():
|
||
if not isinstance(item, dict):
|
||
continue
|
||
if result_only:
|
||
rows.append({"planet": planet, "total": item.get("total") or item.get("shadbala") or item.get("total_virupas")})
|
||
continue
|
||
rows.append({
|
||
"planet": planet,
|
||
"total": _component_number(item.get("sthana_bala") or item.get("sthana")),
|
||
"value": _component_number(item.get("dig_bala") or item.get("dig")),
|
||
"score": _component_number(item.get("kala_bala") or item.get("kala")),
|
||
"years": _component_number(item.get("chesta_bala") or item.get("cheshta_bala") or item.get("chesta")),
|
||
"degree": _component_number(item.get("naisargika_bala") or item.get("naisargika")),
|
||
"orb": _component_number(item.get("drik_bala") or item.get("drik")),
|
||
})
|
||
columns = [("planet", "planet"), ("total", "text")] if result_only else [
|
||
("planet", "planet"), ("total", "text"), ("value", "text"), ("score", "text"),
|
||
("years", "text"), ("degree", "text"), ("orb", "text"),
|
||
]
|
||
return _rows_from(rows, language, columns)
|
||
|
||
|
||
def _coordinates(packet: dict, language: str) -> str:
|
||
core = _dig(packet, ("core_chart", "planets")) or {}
|
||
rows = []
|
||
for planet, item in _shadbala_planets(packet).items():
|
||
kala = item.get("kala_bala") if isinstance(item, dict) else None
|
||
declination = kala.get("declination") if isinstance(kala, dict) else None
|
||
speed = core.get(planet, {}).get("speed") if isinstance(core.get(planet), dict) else None
|
||
rows.append({"planet": planet, "declination": declination, "speed": speed})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("declination", "text"), ("speed", "text")])
|
||
|
||
|
||
def _bhava(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("strengths_and_scores", "bhava_bala")) or {}
|
||
houses = module.get("houses") if isinstance(module, dict) else None
|
||
rows = []
|
||
for item in houses or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"house": item.get("house"),
|
||
"sign": item.get("sign"),
|
||
"adhipathi": item.get("adhipathi_bala"),
|
||
"dig": item.get("dig_bala"),
|
||
"drik": item.get("drik_bala"),
|
||
"total": item.get("total"),
|
||
})
|
||
return _rows_from(rows, language, [("house", "text"), ("sign", "sign"), ("adhipathi", "text"), ("dig", "text"), ("drik", "text"), ("total", "text")])
|
||
|
||
|
||
def _vimsopaka(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("strengths_and_scores", "vimsopaka")) or {}
|
||
nested = module.get("planets") if isinstance(module, dict) else None
|
||
planets = nested if isinstance(nested, dict) else module
|
||
bands = {
|
||
"High": ("High", "高"),
|
||
"Moderate": ("Moderate", "中"),
|
||
"Limited": ("Limited", "有限"),
|
||
"Low": ("Low", "低"),
|
||
}
|
||
rows = []
|
||
if isinstance(planets, dict):
|
||
for planet, item in planets.items():
|
||
if not isinstance(item, dict) or ("total_score" not in item and "category" not in item):
|
||
continue
|
||
if planet not in _PLANETS and item.get("planet") not in _PLANETS:
|
||
continue
|
||
category = item.get("category")
|
||
pair = bands.get(str(category))
|
||
if pair:
|
||
category = pair[1] if language == "zh" else pair[0]
|
||
rows.append({
|
||
"planet": planet if planet in _PLANETS else item.get("planet"),
|
||
"score": item.get("total_score"),
|
||
"category": category,
|
||
})
|
||
elif isinstance(planets, list):
|
||
rows = [item for item in planets if isinstance(item, dict)]
|
||
return _rows_from(rows, language, [("planet", "planet"), ("score", "text"), ("category", "text")])
|
||
|
||
|
||
def _ashtakavarga(packet: dict) -> dict:
|
||
module = _first(packet, ("strengths_and_scores", "ashtakavarga"))
|
||
return module if isinstance(module, dict) else {}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
def _av_signs(packet: dict, language: str) -> str:
|
||
sav = _ashtakavarga(packet).get("sav")
|
||
scores = sav.get("scores") if isinstance(sav, dict) else sav
|
||
assessment = sav.get("assessment") if isinstance(sav, dict) else None
|
||
rows = []
|
||
if isinstance(scores, dict):
|
||
for sign, score in scores.items():
|
||
extra = assessment.get(sign) if isinstance(assessment, dict) else None
|
||
rows.append({"sign": sign, "score": score, "status": extra if not isinstance(extra, dict) else extra.get("assessment") or extra.get("status")})
|
||
return _rows_from(rows, language, [("sign", "sign"), ("score", "text"), ("status", "text")])
|
||
|
||
|
||
def _prastara(packet: dict, language: str) -> str:
|
||
av = _ashtakavarga(packet)
|
||
prastara = av.get("prastara") or av.get("bav")
|
||
rows = []
|
||
if isinstance(prastara, dict):
|
||
for planet, item in prastara.items():
|
||
if isinstance(item, dict):
|
||
total = item.get("total")
|
||
if total is None and all(isinstance(value, (int, float)) for value in item.values()):
|
||
total = sum(value for value in item.values() if isinstance(value, (int, float)))
|
||
rows.append({"planet": planet, "total": total})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("total", "text")])
|
||
|
||
|
||
def _avastha_state(item: dict) -> Any:
|
||
states = item.get("avasthas") if isinstance(item.get("avasthas"), dict) else {}
|
||
for key in ("Jagrat", "Deeptadi"):
|
||
entry = states.get(key)
|
||
if isinstance(entry, dict) and entry.get("state"):
|
||
return entry.get("state")
|
||
return item.get("jagradadi") or item.get("state") or item.get("avastha")
|
||
|
||
|
||
def _avastha(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "avasthas")) or {}
|
||
nested = module.get("planets") if isinstance(module, dict) else None
|
||
planets = nested if isinstance(nested, dict) else module if isinstance(module, dict) else {}
|
||
rows = []
|
||
for planet, item in planets.items():
|
||
if planet not in _PLANETS or not isinstance(item, dict):
|
||
continue
|
||
degree = item.get("degree")
|
||
if degree is None:
|
||
degree = item.get("degree_in_sign")
|
||
rows.append({
|
||
"planet": planet,
|
||
"sign": item.get("sign"),
|
||
"status": _avastha_state(item),
|
||
"degree": degree,
|
||
})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("status", "text"), ("degree", "text")])
|
||
|
||
|
||
def _d30(packet: dict, language: str, source: bool) -> str:
|
||
if source:
|
||
rows = []
|
||
for planet, item in _shadbala_planets(packet).items():
|
||
if planet not in _PLANETS or not isinstance(item, dict):
|
||
continue
|
||
sthana = item.get("sthana_bala") if isinstance(item.get("sthana_bala"), dict) else {}
|
||
score = sthana.get("sapta_d30")
|
||
if isinstance(score, bool) or not isinstance(score, (int, float)):
|
||
continue
|
||
rows.append({"planet": planet, "score": score})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("score", "text")])
|
||
chart = _varga_chart(packet, "D30_Trimsamsa", "D30", "Trimsamsa")
|
||
if not chart:
|
||
legacy = _first(packet, ("advanced_systems", "trimshamsa_d30"))
|
||
chart = legacy if isinstance(legacy, dict) else {}
|
||
dignity = chart.get("_dignity") if isinstance(chart.get("_dignity"), dict) else {}
|
||
rows = []
|
||
for planet, item in _named_planets(chart):
|
||
status = dignity.get(planet) or item.get("dignity") or item.get("status")
|
||
rows.append({
|
||
"planet": planet,
|
||
"sign": item.get("sign"),
|
||
"status": _dignity_token(status, language),
|
||
})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("status", "text")])
|
||
|
||
|
||
def _pushkara(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("strengths_and_scores", "pushkara")) or {}
|
||
rows = []
|
||
if isinstance(module, dict):
|
||
for planet, item in module.items():
|
||
if not isinstance(item, dict):
|
||
continue
|
||
navamsa = item.get("pushkara_navamsa_meta") if isinstance(item.get("pushkara_navamsa_meta"), dict) else {}
|
||
bhaga = item.get("pushkara_bhaga_meta") if isinstance(item.get("pushkara_bhaga_meta"), dict) else {}
|
||
rows.append({
|
||
"planet": planet,
|
||
"status": navamsa.get("is_pushkara") if "is_pushkara" in navamsa else item.get("pushkara_navamsa"),
|
||
"value": bhaga.get("is_pushkara_bhaga") if "is_pushkara_bhaga" in bhaga else item.get("pushkara_bhaga"),
|
||
})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("status", "text"), ("value", "text")])
|
||
|
||
|
||
|
||
|
||
def _goel(packet: dict, language: str) -> str:
|
||
jaimini = _first(packet, ("advanced_systems", "jaimini")) or {}
|
||
strength = jaimini.get("goel_full_strength") if isinstance(jaimini, dict) else {}
|
||
points = jaimini.get("goel_special_points") if isinstance(jaimini, dict) else {}
|
||
if not isinstance(strength, dict):
|
||
strength = {}
|
||
citation = (
|
||
"出处:V. P. Goel《Predicting through Jaimini Astrology》,印刷页 10–17。"
|
||
if language == "zh"
|
||
else "Source: V. P. Goel, Predicting through Jaimini Astrology, printed pages 10-17."
|
||
)
|
||
policy = show(strength.get("node_occupancy_policy"), language)
|
||
ak = show(strength.get("atmakaraka"), language, "planet")
|
||
intro = _rows_from([{"policy": policy, "atmakaraka": ak, "status": strength.get("status")}], language, [("policy", "text"), ("atmakaraka", "text"), ("status", "text")])
|
||
planet_rows = []
|
||
example = strength.get("worked_example_node_exclusion") if isinstance(strength.get("worked_example_node_exclusion"), dict) else {}
|
||
example_planets = example.get("planets") if isinstance(example.get("planets"), dict) else {}
|
||
for planet, item in (strength.get("planets") or {}).items() if isinstance(strength.get("planets"), dict) else []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
example_total = example_planets.get(planet, {}).get("total") if isinstance(example_planets.get(planet), dict) else None
|
||
planet_rows.append({
|
||
"planet": planet,
|
||
"sign": item.get("sign_idx"),
|
||
"degree": item.get("degree_in_sign"),
|
||
"dignity": item.get("dignity"),
|
||
"total": item.get("total"),
|
||
"example_total": example_total,
|
||
})
|
||
rashi_rows = []
|
||
example_rashi = example.get("rashi_strengths") if isinstance(example.get("rashi_strengths"), list) else []
|
||
example_by_sign = {row.get("sign"): row.get("total") for row in example_rashi if isinstance(row, dict)}
|
||
for item in strength.get("rashi_strengths") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
occupants = item.get("occupants")
|
||
if isinstance(occupants, list) and not occupants:
|
||
occupant_cell = "无" if language == "zh" else "None"
|
||
elif isinstance(occupants, list):
|
||
occupant_cell = planet_list(occupants, language) or ("无" if language == "zh" else "None")
|
||
else:
|
||
occupant_cell = occupants
|
||
rashi_rows.append({
|
||
"sign": item.get("sign"),
|
||
"lord": item.get("lord"),
|
||
"permanent": item.get("permanent"),
|
||
"occupant": occupant_cell,
|
||
"total": item.get("total"),
|
||
"example_total": example_by_sign.get(item.get("sign")),
|
||
})
|
||
point_rows = []
|
||
if isinstance(points, dict):
|
||
for name, item in points.items():
|
||
if not isinstance(item, dict):
|
||
continue
|
||
point_rows.append({
|
||
"point": name,
|
||
"status": item.get("status"),
|
||
"value": item.get("value"),
|
||
"rule": item.get("rule"),
|
||
})
|
||
return "\n\n".join([
|
||
citation,
|
||
intro,
|
||
_rows_from(planet_rows, language, [("planet", "planet"), ("sign", "sign"), ("degree", "text"), ("dignity", "text"), ("total", "text"), ("example_total", "text")]),
|
||
_rows_from(rashi_rows, language, [("sign", "sign"), ("lord", "planet"), ("permanent", "text"), ("occupant", "text"), ("total", "text"), ("example_total", "text")]),
|
||
_rows_from(point_rows, language, [("point", "text"), ("status", "text"), ("value", "planet"), ("rule", "text")]),
|
||
])
|
||
|
||
|
||
def _friendship(packet: dict, language: str) -> str:
|
||
snapshot = _first(packet, ("strengths_and_scores", "planetary_friendship"))
|
||
headers, rows = friendship_table(snapshot, language)
|
||
if not rows:
|
||
return _blank(language, ["planet", "friends", "enemies", "neutral", "compound"])
|
||
return _md_table(headers, rows)
|
||
|
||
|
||
|
||
|
||
def _chain_rows(module: Any, terminal: bool) -> list[dict]:
|
||
rows = []
|
||
if isinstance(module, dict):
|
||
items = module.get("chains") or module.get("terminals") or module
|
||
if isinstance(items, dict):
|
||
for planet, item in items.items():
|
||
if isinstance(item, list) and item:
|
||
last = item[-1] if terminal else item[0]
|
||
if isinstance(last, dict):
|
||
rows.append({"planet": planet, "sign": last.get("sign"), "lord": last.get("dispositor") or last.get("planet")})
|
||
elif isinstance(item, dict):
|
||
rows.append({"planet": planet, "sign": item.get("sign") or item.get("final_sign"), "lord": item.get("final") or item.get("dispositor") or item.get("terminal")})
|
||
elif isinstance(items, list):
|
||
rows = [item for item in items if isinstance(item, dict)]
|
||
return rows
|
||
|
||
|
||
def _dispositor(packet: dict, language: str, terminal: bool) -> str:
|
||
if not terminal:
|
||
module = _first(packet, ("advanced_systems", "dispositor_chains"))
|
||
return _rows_from(_chain_rows(module, False), language, [("planet", "planet"), ("sign", "sign"), ("lord", "planet")])
|
||
module = _first(packet, ("advanced_systems", "final_dispositors")) or {}
|
||
rows = []
|
||
if isinstance(module, dict):
|
||
for planet, value in module.items():
|
||
if planet not in _PLANETS:
|
||
continue
|
||
if _is_planet(value):
|
||
rows.append({"planet": planet, "lord": value})
|
||
elif isinstance(value, str) and value.startswith("Cycle:"):
|
||
label = _cycle_label(value, language)
|
||
if label:
|
||
rows.append({"planet": planet, "value": label})
|
||
elif isinstance(value, (dict, list)):
|
||
rows.extend(_chain_rows({planet: value}, True))
|
||
if not rows:
|
||
rows = _chain_rows(module, True)
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("lord", "planet"), ("value", "text")])
|
||
|
||
|
||
def _aspects(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "aspects")) or {}
|
||
items = module.get("aspects") if isinstance(module, dict) else module
|
||
rows = []
|
||
for item in items or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"planet": item.get("planet1") or item.get("planet"),
|
||
"value": item.get("planet2") or item.get("target"),
|
||
"aspect": item.get("type") or item.get("aspect"),
|
||
"orb": item.get("orb") or item.get("actual_diff"),
|
||
})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("value", "planet"), ("aspect", "text"), ("orb", "text")])
|
||
|
||
|
||
|
||
|
||
def _geometry(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "marriage_counting")) or {}
|
||
if not isinstance(module, dict):
|
||
return _blank(language, ["name", "value"])
|
||
rows = []
|
||
for key in ("d1_7th_lord", "point_A", "point_B", "distance", "parivartana"):
|
||
if key in module:
|
||
kind = "planet" if "lord" in key or key.startswith("point") else "text"
|
||
rows.append({"name": show(key, language) if key in ENUMS else ("七宫主" if language == "zh" and key == "d1_7th_lord" else key.replace("_", " ")), "value": module.get(key) if kind == "text" else module.get(key)})
|
||
# The field marriage_count is a geometry output, not a verified event count.
|
||
note = (
|
||
"距离只描述点与点的几何关系,不把它当成已发生的婚姻次数。"
|
||
if language == "zh"
|
||
else "The distance describes geometry between the points. It is not a verified count of marriages."
|
||
)
|
||
table = _rows_from([
|
||
{"name": "七宫主" if language == "zh" else "7th lord", "value": module.get("d1_7th_lord")},
|
||
{"name": "A", "value": module.get("point_A")},
|
||
{"name": "B", "value": module.get("point_B")},
|
||
{"name": "距离" if language == "zh" else "Distance", "value": module.get("distance")},
|
||
], language, [("name", "text"), ("value", "text")])
|
||
return note + "\n\n" + table
|
||
|
||
|
||
def _public_yoga_name(item: dict, language: str) -> str:
|
||
try:
|
||
from scripts.pl9_interpretation_packs import _yoga_name
|
||
except ModuleNotFoundError: # pragma: no cover
|
||
from pl9_interpretation_packs import _yoga_name
|
||
return _yoga_name(item, language)
|
||
|
||
|
||
def _yoga_participants(packet: dict, language: str) -> str:
|
||
yoga = _first(packet, ("advanced_systems", "yoga")) or {}
|
||
pack = yoga.get("interpretation_pack") if isinstance(yoga, dict) else {}
|
||
pages = pack.get("pages") if isinstance(pack, dict) else None
|
||
rows = []
|
||
for page in pages or []:
|
||
if not isinstance(page, dict):
|
||
continue
|
||
rows.append({"name": _public_yoga_name(page, language) if page.get("name") or page.get("name_cn") else undetermined(language), "planet": page.get("planets")})
|
||
if not rows:
|
||
detected = yoga.get("detected_yogas") if isinstance(yoga, dict) else []
|
||
for item in detected or []:
|
||
if isinstance(item, dict):
|
||
rows.append({"name": _public_yoga_name(item, language), "planet": item.get("planets")})
|
||
cleaned = []
|
||
for row in rows:
|
||
name = show(row.get("name"), language)
|
||
if language == "zh" and re.search(r"\b(in|and|or|from|planet|lord)\b", str(row.get("name") or ""), re.IGNORECASE):
|
||
name = undetermined(language)
|
||
cleaned.append({"name": name, "planet": row.get("planet")})
|
||
return _rows_from(cleaned, language, [("name", "text"), ("planet", "planets")])
|
||
|
||
|
||
YOGA_CATEGORIES: dict[str, tuple[str, str]] = {
|
||
"raja": ("Raja", "王瑜伽"),
|
||
"dhana": ("Wealth", "财富瑜伽"),
|
||
"kalatra": ("Marriage", "婚恋"),
|
||
"durbhaga": ("Adverse", "不利组合"),
|
||
"nabhasa": ("Nabhasa", "天空瑜伽"),
|
||
"mahapurusha": ("Mahapurusha", "大人格瑜伽"),
|
||
"pancha_mahapurusha": ("Mahapurusha", "大人格瑜伽"),
|
||
"solar_yoga": ("Solar", "太阳瑜伽"),
|
||
"lunar_yoga": ("Lunar", "月亮瑜伽"),
|
||
"surya": ("Solar", "太阳相关"),
|
||
"chandra": ("Lunar", "月亮相关"),
|
||
"chandra_extended": ("Lunar", "月亮相关"),
|
||
"special": ("Special", "特殊组合"),
|
||
"auspicious": ("Auspicious", "吉祥组合"),
|
||
"putra": ("Children", "子女"),
|
||
"conjunction": ("Conjunction", "合相"),
|
||
"health": ("Health", "健康"),
|
||
"ayur": ("Constitution", "体质"),
|
||
"karma": ("Career", "事业"),
|
||
"moksha": ("Liberation", "解脱"),
|
||
"aryama": ("Aryama", "Aryama"),
|
||
"asha": ("Wishes", "愿望"),
|
||
"guru": ("Jupiter", "木星相关"),
|
||
"neecha_bhanga": ("Debilitation cancellation", "落陷取消"),
|
||
"vidya": ("Learning", "学业"),
|
||
"configuration": ("Configuration", "格局"),
|
||
"travel": ("Travel", "远行"),
|
||
"ari": ("Opponents", "对手"),
|
||
"relationship_observation": ("Relationship observation", "关系观察"),
|
||
"shakti": ("Shakti", "Shakti"),
|
||
"varga": ("Divisional", "分盘"),
|
||
}
|
||
|
||
|
||
def _canonical_yogas(packet: dict, language: str) -> str:
|
||
yoga = _first(packet, ("advanced_systems", "yoga")) or {}
|
||
rows = []
|
||
for item in yoga.get("detected_yogas") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"name": _public_yoga_name(item, language),
|
||
"category": _pair(YOGA_CATEGORIES, item.get("category"), language),
|
||
"planet": item.get("planets"),
|
||
})
|
||
return _rows_from(rows, language, [("name", "text"), ("category", "text"), ("planet", "planets")])
|
||
|
||
|
||
def _occupants(packet: dict, language: str) -> str:
|
||
d1 = _first(packet, ("d1_rasi_bhava",)) or {}
|
||
planets = d1.get("planets") if isinstance(d1, dict) else {}
|
||
houses: dict[str, list[str]] = {}
|
||
if isinstance(planets, dict):
|
||
for planet, item in planets.items():
|
||
if isinstance(item, dict) and item.get("house"):
|
||
houses.setdefault(str(item.get("house")), []).append(str(planet))
|
||
rows = [{"house": house, "planet": names} for house, names in sorted(houses.items(), key=lambda pair: int(pair[0]) if str(pair[0]).isdigit() else 99)]
|
||
return _rows_from(rows, language, [("house", "text"), ("planet", "planets")])
|
||
|
||
|
||
def _nakshatra_reference(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "nakshatra_adv")) or {}
|
||
natal = module.get("natal_nakshatras") if isinstance(module, dict) else None
|
||
rows = []
|
||
if isinstance(natal, dict):
|
||
for planet, item in natal.items():
|
||
if isinstance(item, dict):
|
||
rows.append({"planet": planet, "nakshatra": item.get("nakshatra") or item.get("name"), "pada": item.get("pada"), "lord": item.get("lord")})
|
||
elif isinstance(natal, list):
|
||
rows = [item for item in natal if isinstance(item, dict)]
|
||
return _rows_from(rows, language, [("planet", "planet"), ("nakshatra", "text"), ("pada", "text"), ("lord", "planet")])
|
||
|
||
|
||
|
||
|
||
|
||
|
||
def _saham_basis(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "sahams")) or {}
|
||
rows = []
|
||
if isinstance(module, dict):
|
||
for name, item in module.items():
|
||
if not isinstance(item, dict):
|
||
continue
|
||
if item.get("sign") in (None, "") and not isinstance(item.get("longitude"), (int, float)):
|
||
continue
|
||
label = item.get("name")
|
||
if not isinstance(label, str) or "_" in label or not label.strip():
|
||
label = " ".join(str(name).replace("_", " ").split())
|
||
rows.append({
|
||
"name": label,
|
||
"sign": item.get("sign"),
|
||
"degree": item.get("degree_in_sign"),
|
||
"longitude": item.get("longitude"),
|
||
})
|
||
return _rows_from(rows, language, [("name", "text"), ("sign", "sign"), ("degree", "text"), ("longitude", "text")])
|
||
|
||
|
||
def _special_points(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("divisional_and_special_charts", "special_lagnas")) or {}
|
||
rows = []
|
||
if isinstance(module, dict):
|
||
for name, item in module.items():
|
||
if isinstance(item, dict):
|
||
rows.append({"point": name, "sign": item.get("sign"), "degree": item.get("degree_in_sign") or item.get("degree")})
|
||
wanted = {
|
||
"Arudha_Lagna": ("Arudha lagna", "Arudha 上升"),
|
||
"Upapada_Lagna": ("Upapada lagna", "Upapada 上升"),
|
||
"A10": ("A10", "A10"),
|
||
"Arudha": ("Arudha", "Arudha"),
|
||
"UL": ("Upapada", "Upapada"),
|
||
}
|
||
picked = []
|
||
for row in rows:
|
||
pair = wanted.get(str(row["point"]))
|
||
if pair:
|
||
row = dict(row)
|
||
row["point"] = pair[1] if language == "zh" else pair[0]
|
||
picked.append(row)
|
||
picked = picked or rows
|
||
return _rows_from(picked, language, [("point", "text"), ("sign", "sign"), ("degree", "text")])
|
||
|
||
|
||
|
||
|
||
def _graha_pada(packet: dict, language: str) -> str:
|
||
jaimini = _first(packet, ("advanced_systems", "jaimini")) or {}
|
||
padas = jaimini.get("graha_padas") if isinstance(jaimini, dict) else {}
|
||
inner = padas.get("graha_padas") if isinstance(padas, dict) and isinstance(padas.get("graha_padas"), dict) else {}
|
||
rows = []
|
||
for planet, item in inner.items():
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"planet": planet,
|
||
"sign": item.get("graha_pada_sign") or item.get("sign"),
|
||
"lord": item.get("lord"),
|
||
})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("lord", "planet")])
|
||
|
||
|
||
def _one_point(packet: dict, language: str, key: str) -> str:
|
||
jaimini = _first(packet, ("advanced_systems", "jaimini")) or {}
|
||
item = jaimini.get(key) if isinstance(jaimini, dict) else None
|
||
if isinstance(item, str):
|
||
return _rows_from([{"planet": item}], language, [("planet", "planet")])
|
||
if not isinstance(item, dict):
|
||
return _blank(language, ["planet", "sign"])
|
||
if key == "karakamsha":
|
||
return _rows_from([{
|
||
"sign": item.get("karakamsha_sign") or item.get("sign"),
|
||
"degree": item.get("karakamsha_degree") if "karakamsha_degree" in item else item.get("degree"),
|
||
"lord": item.get("karakamsha_lord") or item.get("lord") or item.get("planet"),
|
||
"house": item.get("house"),
|
||
}], language, [("sign", "sign"), ("degree", "text"), ("lord", "planet"), ("house", "text")])
|
||
if key == "darakaraka":
|
||
return _rows_from([{
|
||
"planet": item.get("dk_planet") or item.get("planet"),
|
||
"sign": item.get("dk_sign") or item.get("sign"),
|
||
"house": item.get("dk_house") if item.get("dk_house") is not None else item.get("house"),
|
||
"degree": item.get("dk_degree") if item.get("dk_degree") is not None else item.get("degree"),
|
||
"value": item.get("d9_sign"),
|
||
"dignity": _dignity_token(item.get("d9_dignity") or item.get("dignity"), language),
|
||
}], language, [("planet", "planet"), ("sign", "sign"), ("house", "text"), ("degree", "text"), ("value", "sign"), ("dignity", "text")])
|
||
return _rows_from([item], language, [("planet", "planet"), ("sign", "sign"), ("house", "text")])
|
||
|
||
|
||
def _rashi_tulya(packet: dict, language: str) -> str:
|
||
del packet
|
||
return _missing_module(language, ["name", "value"])
|
||
|
||
|
||
def _moon(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("divisional_and_special_charts", "moon_chart")) or {}
|
||
planets = module.get("planets") if isinstance(module, dict) else None
|
||
if planets is None and isinstance(module.get("raw"), dict):
|
||
planets = module["raw"].get("planets")
|
||
rows = []
|
||
if isinstance(planets, dict):
|
||
for planet, item in planets.items():
|
||
if isinstance(item, dict):
|
||
rows.append({"planet": planet, "sign": item.get("sign"), "house": item.get("house") or item.get("house_from_moon")})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("house", "text")])
|
||
|
||
|
||
def _sudarshana(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("divisional_and_special_charts", "sudarshana")) or {}
|
||
raw = module.get("raw") if isinstance(module, dict) and isinstance(module.get("raw"), dict) else {}
|
||
points = raw.get("reference_points") if isinstance(raw.get("reference_points"), dict) else None
|
||
if not isinstance(points, dict) and isinstance(module, dict):
|
||
points = module.get("reference_points")
|
||
rows = []
|
||
labels = {
|
||
"ascendant_lagna": ("Ascendant reference", "出生上升参考点"),
|
||
"moon_lagna": ("Moon reference", "月亮参考点"),
|
||
"sun_lagna": ("Sun reference", "太阳参考点"),
|
||
}
|
||
if isinstance(points, dict):
|
||
for key, item in points.items():
|
||
if isinstance(item, dict):
|
||
pair = labels.get(key)
|
||
rows.append({"point": (pair[1] if language == "zh" else pair[0]) if pair else undetermined(language), "sign": item.get("sign")})
|
||
return _rows_from(rows, language, [("point", "text"), ("sign", "sign")])
|
||
|
||
|
||
def _transit(packet: dict, language: str) -> str:
|
||
multi = _first(packet, ("timing_and_predictive_systems", "transit_multi_reference")) or {}
|
||
analysis = multi.get("transit_analysis") if isinstance(multi, dict) else {}
|
||
when = multi.get("target_date") if isinstance(multi, dict) else None
|
||
overlay_root = _first(packet, ("advanced_systems", "nakshatra_dasha")) or {}
|
||
overlay = overlay_root.get("transit_overlay") if isinstance(overlay_root, dict) else {}
|
||
if not isinstance(analysis, dict):
|
||
analysis = {}
|
||
if not isinstance(overlay, dict):
|
||
overlay = {}
|
||
order = [planet for planet in (*analysis.keys(), *overlay.keys()) if planet in _PLANETS]
|
||
seen = []
|
||
for planet in order:
|
||
if planet not in seen:
|
||
seen.append(planet)
|
||
rows = []
|
||
for planet in seen:
|
||
item = analysis.get(planet) if isinstance(analysis.get(planet), dict) else {}
|
||
over = overlay.get(planet) if isinstance(overlay.get(planet), dict) else {}
|
||
nakshatra = over.get("transit_nakshatra")
|
||
if isinstance(nakshatra, str) and "_" in nakshatra:
|
||
nakshatra = nakshatra.replace("_", " ")
|
||
if not isinstance(nakshatra, str):
|
||
nakshatra = None
|
||
rows.append({
|
||
"planet": planet,
|
||
"sign": item.get("sign"),
|
||
"degree": item.get("degree_in_sign"),
|
||
"date": when if isinstance(when, (str, int, float)) else item.get("date"),
|
||
"nakshatra": nakshatra,
|
||
})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("degree", "text"), ("date", "text"), ("nakshatra", "text")])
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
def _settings(packet: dict, language: str) -> str:
|
||
birth = packet.get("birth_info") if isinstance(packet.get("birth_info"), dict) else {}
|
||
profile = packet.get("calculation_profile") if isinstance(packet.get("calculation_profile"), dict) else {}
|
||
row = {
|
||
"name": birth.get("ayanamsa_display") or birth.get("ayanamsa_name") or profile.get("ayanamsa"),
|
||
"value": birth.get("node_mode") or profile.get("node_mode"),
|
||
"method": profile.get("house_system") or "whole sign",
|
||
}
|
||
return _rows_from([row], language, [("name", "text"), ("value", "text"), ("method", "text")])
|
||
|
||
|
||
|
||
|
||
def _d9(packet: dict, language: str) -> str:
|
||
chart = _varga_chart(packet, "D9_Navamsa", "D9", "d9", "Navamsa", "navamsa")
|
||
dignity = chart.get("_dignity") if isinstance(chart.get("_dignity"), dict) else {}
|
||
rows = []
|
||
for planet, item in _named_planets(chart):
|
||
rows.append({
|
||
"planet": planet,
|
||
"sign": item.get("sign"),
|
||
"dignity": _dignity_token(dignity.get(planet) or item.get("dignity") or item.get("status"), language),
|
||
})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("dignity", "text")])
|
||
|
||
|
||
def _mapping(packet: dict, language: str) -> str:
|
||
varga = _first(packet, ("divisional_and_special_charts", "varga_full")) or {}
|
||
rows = []
|
||
if isinstance(varga, dict):
|
||
for name, chart in varga.items():
|
||
if not isinstance(chart, dict):
|
||
continue
|
||
if any(token in str(name).upper() for token in ("D81", "D108", "D144")):
|
||
continue
|
||
asc = chart.get("Ascendant") if isinstance(chart.get("Ascendant"), dict) else {}
|
||
if not asc and isinstance(chart.get("ascendant"), dict):
|
||
asc = chart.get("ascendant") or {}
|
||
sign = asc.get("sign") or chart.get("asc_sign") or chart.get("lagna_sign")
|
||
if sign in (None, "") and isinstance(asc.get("sign_idx"), int):
|
||
sign = asc.get("sign_idx")
|
||
if sign in (None, ""):
|
||
continue
|
||
rows.append({"name": _varga_label(str(name), chart), "sign": sign})
|
||
return _rows_from(rows, language, [("name", "text"), ("sign", "sign")])
|
||
|
||
|
||
def _bhava_raw(packet: dict) -> dict:
|
||
module = _first(packet, ("divisional_and_special_charts", "bhava_chalit")) or {}
|
||
raw = module.get("raw") if isinstance(module, dict) else None
|
||
return raw if isinstance(raw, dict) else module if isinstance(module, dict) else {}
|
||
|
||
|
||
def _chalit_planets(packet: dict, language: str) -> str:
|
||
raw = _bhava_raw(packet)
|
||
rows = []
|
||
for planet, item in (raw.get("planets") or {}).items():
|
||
if isinstance(item, dict):
|
||
sign = item.get("sign_cn") if language == "zh" and item.get("sign_cn") else item.get("sign")
|
||
rows.append({"planet": planet, "sign": sign, "rashi_house": item.get("rashi_house"), "bhava_house": item.get("bhava_house"), "degree": item.get("degree_in_sign")})
|
||
return _rows_from(rows, language, [("planet", "planet"), ("sign", "sign"), ("rashi_house", "text"), ("bhava_house", "text"), ("degree", "text")])
|
||
|
||
|
||
def _chalit_centres(packet: dict, language: str) -> str:
|
||
raw = _bhava_raw(packet)
|
||
boundaries = raw.get("boundaries") if isinstance(raw.get("boundaries"), dict) else {}
|
||
details = boundaries.get("houses_detail") if isinstance(boundaries.get("houses_detail"), list) else None
|
||
if not details:
|
||
details = boundaries.get("houses") if isinstance(boundaries.get("houses"), list) else []
|
||
rows = []
|
||
for item in details or []:
|
||
if isinstance(item, dict):
|
||
sign = item.get("cusp_sign_cn") if language == "zh" and item.get("cusp_sign_cn") else item.get("cusp_sign")
|
||
rows.append({"house": item.get("house"), "sign": sign, "degree": item.get("cusp_degree_in_sign"), "longitude": item.get("cusp_lon")})
|
||
return _rows_from(rows, language, [("house", "text"), ("sign", "sign"), ("degree", "text"), ("longitude", "text")])
|
||
|
||
|
||
def _chalit_convention(packet: dict, language: str) -> str:
|
||
raw = _bhava_raw(packet)
|
||
system = raw.get("house_system")
|
||
assignment = raw.get("house_assignment")
|
||
note = (
|
||
"宫位用 Sripati 宫心。行星落宫按交界,不按宫头到宫头。"
|
||
if language == "zh"
|
||
else "Houses use Sripati centres. A planet's house is read from the sandhi, not from cusp to cusp."
|
||
)
|
||
if system and str(system).lower() != "sripati":
|
||
note = show(system, language)
|
||
row = {"method": note, "value": assignment}
|
||
return _rows_from([row], language, [("method", "text"), ("value", "text")])
|
||
|
||
|
||
|
||
|
||
def _vivah(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "vivah_saham"))
|
||
if not isinstance(module, dict):
|
||
sahams = _first(packet, ("advanced_systems", "sahams")) or {}
|
||
module = sahams.get("vivah_saham") if isinstance(sahams, dict) else {}
|
||
if not isinstance(module, dict):
|
||
return _blank(language, ["sign", "degree", "longitude"])
|
||
return _rows_from([module], language, [("sign", "sign"), ("degree_in_sign", "text"), ("longitude", "text"), ("degree", "text")])
|
||
|
||
|
||
def _pair(table: dict, key: Any, language: str) -> str | None:
|
||
pair = table.get(str(key)) if key is not None else None
|
||
if not pair:
|
||
return None
|
||
return pair[1] if language == "zh" else pair[0]
|
||
|
||
|
||
def _items_table(rows: list[tuple[tuple[str, str], Any, str]], language: str) -> str:
|
||
"""Two columns: a fixed bilingual label and the packet value."""
|
||
body = []
|
||
for (label_en, label_zh), value, kind in rows:
|
||
body.append([label_zh if language == "zh" else label_en, show(value, language, kind)])
|
||
if not any(cell != undetermined(language) for _label, cell in body):
|
||
return _blank(language, ["item", "value"])
|
||
return _md_table([lab("item", language), lab("value", language)], body)
|
||
|
||
|
||
def _bav_sum(values: Any) -> Any:
|
||
if isinstance(values, list) and values and all(isinstance(item, (int, float)) and not isinstance(item, bool) for item in values):
|
||
return sum(values)
|
||
return None
|
||
|
||
|
||
def _av_reduction(packet: dict, language: str) -> str:
|
||
av = _ashtakavarga(packet)
|
||
sodhita = av.get("sodhita") if isinstance(av.get("sodhita"), dict) else {}
|
||
shodhya = av.get("shodhya_pinda") if isinstance(av.get("shodhya_pinda"), dict) else {}
|
||
raw = sodhita.get("raw_bav") if isinstance(sodhita.get("raw_bav"), dict) else {}
|
||
trikona = sodhita.get("trikona_bav") if isinstance(sodhita.get("trikona_bav"), dict) else {}
|
||
reduced = sodhita.get("sodhita_bav") if isinstance(sodhita.get("sodhita_bav"), dict) else {}
|
||
rows = []
|
||
for planet in _PLANETS:
|
||
if planet not in raw and planet not in shodhya:
|
||
continue
|
||
pinda = shodhya.get(planet) if isinstance(shodhya.get(planet), dict) else {}
|
||
rows.append({
|
||
"planet": planet,
|
||
"raw_total": _bav_sum(raw.get(planet)),
|
||
"trikona_total": _bav_sum(trikona.get(planet)),
|
||
"sodhita_total": _bav_sum(reduced.get(planet)),
|
||
"rashi_pinda": pinda.get("rashi_pinda"),
|
||
"graha_pinda": pinda.get("graha_pinda"),
|
||
"shodhya_pinda": pinda.get("total_pinda"),
|
||
})
|
||
table = _rows_from(rows, language, [
|
||
("planet", "planet"), ("raw_total", "text"), ("trikona_total", "text"), ("sodhita_total", "text"),
|
||
("rashi_pinda", "text"), ("graha_pinda", "text"), ("shodhya_pinda", "text"),
|
||
])
|
||
audit = []
|
||
for item in sodhita.get("ekadhipatya_audit") or []:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
pair = item.get("pair") if isinstance(item.get("pair"), list) else []
|
||
before = item.get("before") if isinstance(item.get("before"), list) else []
|
||
after = item.get("after") if isinstance(item.get("after"), list) else []
|
||
if len(pair) != 2 or len(before) != 2 or len(after) != 2:
|
||
continue
|
||
signs = [show(index, language, "sign") for index in pair]
|
||
joiner = "、" if language == "zh" else " / "
|
||
audit.append({
|
||
"planet": item.get("planet"),
|
||
"signs": joiner.join(signs),
|
||
"before": f"{before[0]} / {before[1]}",
|
||
"after": f"{after[0]} / {after[1]}",
|
||
})
|
||
parts = [table]
|
||
if audit:
|
||
note = (
|
||
"下表是单宫主净化(Ekadhipatya)实际改动的星座对。"
|
||
if language == "zh"
|
||
else "The rows below are the sign pairs changed by Ekadhipatya reduction."
|
||
)
|
||
parts += [note, _rows_from(audit, language, [("planet", "planet"), ("signs", "text"), ("before", "text"), ("after", "text")])]
|
||
return "\n\n".join(parts)
|
||
|
||
|
||
def _av_pinda(packet: dict, language: str) -> str:
|
||
pinda = _ashtakavarga(packet).get("yoga_pinda")
|
||
if not isinstance(pinda, dict):
|
||
return _blank(language, ["planet", "yoga_pinda"])
|
||
planets = pinda.get("planets") if isinstance(pinda.get("planets"), dict) else {}
|
||
weights = pinda.get("planet_weights") if isinstance(pinda.get("planet_weights"), dict) else {}
|
||
rows = []
|
||
for planet in _PLANETS:
|
||
item = planets.get(planet)
|
||
if not isinstance(item, dict):
|
||
continue
|
||
rows.append({
|
||
"planet": planet,
|
||
"sign": item.get("sign"),
|
||
"own_bindu": item.get("bindu_at_own_sign"),
|
||
"rashi_pinda": item.get("rashi_pinda"),
|
||
"graha_pinda": item.get("graha_pinda"),
|
||
"yoga_pinda": item.get("yoga_pinda"),
|
||
"weight": weights.get(planet),
|
||
})
|
||
table = _rows_from(rows, language, [
|
||
("planet", "planet"), ("sign", "sign"), ("own_bindu", "text"), ("rashi_pinda", "text"),
|
||
("graha_pinda", "text"), ("yoga_pinda", "text"), ("weight", "text"),
|
||
])
|
||
summary = pinda.get("summary") if isinstance(pinda.get("summary"), dict) else {}
|
||
if not summary:
|
||
return table
|
||
totals = _items_table([
|
||
(("Strongest planet", "最强行星"), summary.get("strongest_planet"), "planet"),
|
||
(("Weakest planet", "最弱行星"), summary.get("weakest_planet"), "planet"),
|
||
(("Total Yoga Pinda", "Yoga Pinda 合计"), summary.get("total_yoga_pinda"), "text"),
|
||
(("Average Yoga Pinda", "Yoga Pinda 平均"), summary.get("average_yoga_pinda"), "text"),
|
||
], language)
|
||
return table + "\n\n" + totals
|
||
|
||
|
||
def _lagna_vargottama(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("strengths_and_scores", "lagna_vargottamamsa")) or {}
|
||
if not isinstance(module, dict) or not module:
|
||
return _blank(language, ["item", "value"])
|
||
flag = module.get("is_lagna_vargottamamsa")
|
||
if flag is None:
|
||
flag = module.get("is_vargottama")
|
||
return _items_table([
|
||
(("D1 ascendant sign", "D1 上升星座"), module.get("d1_sign") or module.get("lagna_sign") or module.get("sign"), "sign"),
|
||
(("D9 ascendant sign", "D9 上升星座"), module.get("d9_sign") or module.get("navamsa_sign"), "sign"),
|
||
(("Lagna Vargottamamsa", "上升 Vargottamamsa"), flag if isinstance(flag, bool) else None, "text"),
|
||
], language)
|
||
|
||
|
||
_COMPOUND_ZH = {
|
||
"local_permanent_relationship_constants_plus_temporary_sign_distance": (
|
||
"Permanent relations from the constant table; temporary relations from the sign distance",
|
||
"天然关系查常量表,临时关系按两星的星座距离",
|
||
),
|
||
}
|
||
|
||
|
||
def _relationship_note(packet: dict, language: str) -> str:
|
||
if language == "zh":
|
||
note = "天然关系来自常量表。临时关系来自两星的星座距离。复合关系由这两层合成,印成极友、友、中性、敌、极敌。"
|
||
else:
|
||
note = "Natural relations come from the permanent table. Temporary relations come from the sign distance of the two planets. The compound relation joins those two layers as great friend, friend, neutral, enemy, or great enemy."
|
||
snapshot = _first(packet, ("strengths_and_scores", "planetary_friendship"))
|
||
if not isinstance(snapshot, dict):
|
||
return note + "\n\n" + _blank(language, ["item", "value"])
|
||
sources = snapshot.get("source_planets") if isinstance(snapshot.get("source_planets"), list) else []
|
||
rows = snapshot.get("rows") if isinstance(snapshot.get("rows"), list) else []
|
||
nodes = [planet for planet in sources if planet in ("Rahu", "Ketu")]
|
||
table = _items_table([
|
||
(("Method", "口径"), _pair(_COMPOUND_ZH, snapshot.get("method"), language), "text"),
|
||
(("Bodies counted", "计入星体"), sources or None, "planet_list"),
|
||
(("Nodes counted", "是否计入交点"), bool(nodes) if sources else None, "text"),
|
||
(("Rows", "行数"), len(rows) if rows else None, "text"),
|
||
(("Status", "状态"), snapshot.get("status"), "text"),
|
||
], language)
|
||
return note + "\n\n" + table
|
||
|
||
|
||
_ARGALA_RESULT = {
|
||
"supported": ("Supported", "支持"),
|
||
"obstructed": ("Obstructed", "受阻"),
|
||
"mixed": ("Mixed", "混合"),
|
||
"neutral": ("Neutral", "中性"),
|
||
"strength_comparison_required": ("Needs a strength comparison", "需比较强弱"),
|
||
}
|
||
|
||
|
||
def _argala(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "argala")) or {}
|
||
houses = module.get("houses") if isinstance(module, dict) else None
|
||
if not isinstance(houses, dict):
|
||
return _blank(language, ["house", "status"])
|
||
rows = []
|
||
for number in range(1, 13):
|
||
item = houses.get(f"house_{number}")
|
||
if not isinstance(item, dict):
|
||
continue
|
||
primary = item.get("primary") if isinstance(item.get("primary"), dict) else {}
|
||
effective = []
|
||
for key, entry in primary.items():
|
||
if isinstance(entry, dict) and entry.get("effective") is True and isinstance(entry.get("house_from_reference"), int):
|
||
effective.append(entry["house_from_reference"])
|
||
joiner = "、" if language == "zh" else ", "
|
||
rows.append({
|
||
"house": number,
|
||
"sign": item.get("ref_sign"),
|
||
"argala": item.get("argala_count"),
|
||
"virodhargala": item.get("virodhargala_count"),
|
||
"effective": joiner.join(str(value) for value in sorted(effective)) if effective else ("无" if language == "zh" else "None"),
|
||
"status": _pair(_ARGALA_RESULT, item.get("net_result"), language),
|
||
})
|
||
return _rows_from(rows, language, [
|
||
("house", "text"), ("sign", "sign"), ("argala", "text"), ("virodhargala", "text"),
|
||
("effective", "text"), ("status", "text"),
|
||
])
|
||
|
||
|
||
_TRANSIT_SLOTS = (
|
||
("natal_lagna", ("Natal ascendant house", "本命上升宫")),
|
||
("natal_house_7", ("Natal 7th house", "本命第七宫")),
|
||
)
|
||
|
||
|
||
def _congregation(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "planetary_congregation"))
|
||
if not isinstance(module, dict) or not module:
|
||
return _blank(language, ["item", "planet"])
|
||
none = "无" if language == "zh" else "None"
|
||
natal = module.get("natal") if isinstance(module.get("natal"), dict) else {}
|
||
transit = module.get("transit") if isinstance(module.get("transit"), dict) else {}
|
||
rows = []
|
||
for key, (label_en, label_zh) in (("lagna", _TRANSIT_SLOTS[0][1]), ("house_7", _TRANSIT_SLOTS[1][1])):
|
||
if isinstance(natal.get(key), list):
|
||
planets = list(dict.fromkeys(natal.get(key)))
|
||
rows.append([label_zh if language == "zh" else label_en, show(planets, language, "planets") if planets else none])
|
||
for number in range(1, 13):
|
||
planets = transit.get(str(number))
|
||
if isinstance(planets, list) and planets:
|
||
planets = list(dict.fromkeys(planets))
|
||
label = f"行运第 {number} 宫" if language == "zh" else f"Transit house {number}"
|
||
rows.append([label, show(planets, language, "planets")])
|
||
if not rows:
|
||
return _blank(language, ["item", "planet"])
|
||
hit = module.get("hit")
|
||
date = module.get("reference_date") or module.get("transit_date")
|
||
lead = _items_table([
|
||
(("Reference date", "参考日"), date, "text"),
|
||
(("Congregation found", "是否有聚集"), hit if isinstance(hit, bool) else None, "text"),
|
||
], language)
|
||
return lead + "\n\n" + _md_table([lab("item", language), lab("planet", language)], rows)
|
||
|
||
|
||
_REFERENCES = {
|
||
"Lagna": ("Ascendant", "上升点"),
|
||
"Chandra_Lagna": ("Moon ascendant", "月亮上升"),
|
||
"Arudha_Lagna": ("Arudha lagna", "Arudha 上升"),
|
||
"Navamsa_Lagna": ("Navamsa ascendant", "D9 上升"),
|
||
}
|
||
|
||
|
||
def _multi_transit(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "transit_multi_reference"))
|
||
if not isinstance(module, dict):
|
||
return _blank(language, ["reference", "sign"])
|
||
references = module.get("references") if isinstance(module.get("references"), dict) else {}
|
||
keys = [key for key in _REFERENCES if key in references]
|
||
reference_rows = []
|
||
for key in keys:
|
||
item = references.get(key) if isinstance(references.get(key), dict) else {}
|
||
reference_rows.append({"point": _pair(_REFERENCES, key, language), "sign": item.get("sign"), "priority": item.get("priority")})
|
||
parts = [_rows_from(reference_rows, language, [("point", "text"), ("sign", "sign"), ("priority", "text")])] if reference_rows else []
|
||
analysis = module.get("transit_analysis") if isinstance(module.get("transit_analysis"), dict) else {}
|
||
headers = [lab("planet", language), lab("sign", language), lab("degree", language)]
|
||
for key in keys:
|
||
name = _pair(_REFERENCES, key, language)
|
||
headers.append(f"从{name}起的宫" if language == "zh" else f"House from {name.lower()}")
|
||
rows = []
|
||
for planet in _PLANETS:
|
||
item = analysis.get(planet)
|
||
if not isinstance(item, dict):
|
||
continue
|
||
houses = item.get("house_from_ref") if isinstance(item.get("house_from_ref"), dict) else {}
|
||
row = [show(planet, language, "planet"), show(item.get("sign"), language, "sign"), show(item.get("degree_in_sign"), language)]
|
||
for key in keys:
|
||
entry = houses.get(key) if isinstance(houses.get(key), dict) else {}
|
||
row.append(show(entry.get("house"), language))
|
||
rows.append(row)
|
||
if rows:
|
||
date = module.get("target_date")
|
||
if date:
|
||
parts.insert(0, f"参考日:{date}。" if language == "zh" else f"Reference date: {date}.")
|
||
parts.append(_md_table(headers, rows))
|
||
if not parts:
|
||
return _blank(language, ["reference", "sign"])
|
||
return "\n\n".join(parts)
|
||
|
||
|
||
_PAC_KIND = {
|
||
"Position": ("Same house", "同宫"),
|
||
"Conjunction": ("Conjunction", "合相"),
|
||
}
|
||
|
||
|
||
def _pac_text(entries: Any, language: str) -> str | None:
|
||
if not isinstance(entries, list):
|
||
return None
|
||
parts = []
|
||
for entry in entries:
|
||
if not isinstance(entry, dict):
|
||
continue
|
||
kind = entry.get("type")
|
||
if kind == "Aspect" and isinstance(entry.get("offset"), int):
|
||
offset = entry["offset"]
|
||
parts.append(f"{offset} 宫相位" if language == "zh" else f"Aspect to the {offset}th")
|
||
continue
|
||
label = _pair(_PAC_KIND, kind, language)
|
||
if label:
|
||
parts.append(label)
|
||
if not parts:
|
||
return None
|
||
return ("、" if language == "zh" else ", ").join(parts)
|
||
|
||
|
||
_TARGET_PATTERNS = (
|
||
(re.compile(r"^(D9_|CL_)?(\d+)宫\((\w+)\)$"), "house"),
|
||
(re.compile(r"^(D9_|CL_)?(\w+)\(宫主\)$"), "lord"),
|
||
(re.compile(r"^(\w+)\(LL\)$"), "lagna_lord"),
|
||
(re.compile(r"^(\w+)\(对宫主\)$"), "opposite_lord"),
|
||
(re.compile(r"^(\w+)_D9\((\w+)\)$"), "d9_sign"),
|
||
)
|
||
|
||
|
||
def _pac_target(name: Any, language: str) -> str | None:
|
||
text = str(name or "")
|
||
for pattern, kind in _TARGET_PATTERNS:
|
||
match = pattern.match(text)
|
||
if not match:
|
||
continue
|
||
if kind == "house":
|
||
house, sign = match.group(2), match.group(3)
|
||
if sign not in SIGNS:
|
||
return None
|
||
sign_text = sign_name(sign, language)
|
||
return f"第 {house} 宫({sign_text})" if language == "zh" else f"House {house} ({sign_text})"
|
||
planet = match.group(2) if kind == "lord" else match.group(1)
|
||
if not _is_planet(planet):
|
||
return None
|
||
planet_text = planet_name(planet, language)
|
||
if kind == "lord":
|
||
return f"{planet_text}(事件宫主)" if language == "zh" else f"{planet_text} (event-house lord)"
|
||
if kind == "lagna_lord":
|
||
return f"{planet_text}(上升主)" if language == "zh" else f"{planet_text} (ascendant lord)"
|
||
if kind == "opposite_lord":
|
||
return f"{planet_text}(七宫主)" if language == "zh" else f"{planet_text} (7th lord)"
|
||
sign = match.group(2)
|
||
if sign not in SIGNS:
|
||
return None
|
||
sign_text = sign_name(sign, language)
|
||
return f"{planet_text} 的 D9 星座({sign_text})" if language == "zh" else f"D9 sign of {planet_text} ({sign_text})"
|
||
return None
|
||
|
||
|
||
_PAC_LAYERS = (
|
||
("d1", ("D1", "D1")),
|
||
("d9", ("D9", "D9")),
|
||
("cl", ("Moon ascendant", "月亮上升")),
|
||
)
|
||
|
||
|
||
def _double_transit(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "double_transit_pac"))
|
||
if not isinstance(module, dict) or not module:
|
||
return _blank(language, ["layer", "target"])
|
||
rows = []
|
||
for key, pair in _PAC_LAYERS:
|
||
layer = module.get(key) if isinstance(module.get(key), dict) else {}
|
||
for mover in ("jupiter", "saturn"):
|
||
targets = layer.get(mover) if isinstance(layer.get(mover), dict) else {}
|
||
for target, entries in targets.items():
|
||
rows.append({
|
||
"layer": pair[1] if language == "zh" else pair[0],
|
||
"planet": mover.title(),
|
||
"target": _pac_target(target, language),
|
||
"aspect": _pac_text(entries, language),
|
||
})
|
||
stats = module.get("stats") if isinstance(module.get("stats"), dict) else {}
|
||
hits = [item for item in module.get("double_transit") or [] if isinstance(item, dict)]
|
||
lead = _items_table([
|
||
(("Reference date", "参考日"), module.get("reference_date") or module.get("transit_date"), "text"),
|
||
(("Event house", "事件宫"), module.get("event_house"), "text"),
|
||
(("D9 ascendant", "D9 上升"), stats.get("d9_ascendant"), "sign"),
|
||
(("Moon ascendant", "月亮上升"), stats.get("chandra_lagna"), "sign"),
|
||
(("Double transit hits", "双重行运命中数"), len(hits) if "double_transit" in module else None, "text"),
|
||
], language)
|
||
parts = [lead]
|
||
if rows:
|
||
parts.append(_rows_from(rows, language, [("layer", "text"), ("planet", "planet"), ("target", "text"), ("aspect", "text")]))
|
||
else:
|
||
parts.append(
|
||
"参考日木星与土星都没有连接事件宫目标。"
|
||
if language == "zh"
|
||
else "Neither transiting Jupiter nor Saturn links an event-house target on the reference date."
|
||
)
|
||
return "\n\n".join(parts)
|
||
|
||
|
||
_DIRECTION = re.compile(r"^Transit (\w+) → natal (\w+)$")
|
||
_P8 = re.compile(r"^Transit (\w+)\((\w+)\)在(7H|Lagna)$")
|
||
_SWAP = re.compile(r"Transit (\w+)在(\w+)")
|
||
|
||
|
||
def _ll7l_detail(kind: str, item: Any, language: str) -> str | None:
|
||
if kind == "p5" and isinstance(item, dict):
|
||
match = _DIRECTION.match(str(item.get("direction") or ""))
|
||
if not match or not all(_is_planet(value) for value in match.groups()):
|
||
return None
|
||
mover, target = (planet_name(value, language) for value in match.groups())
|
||
pac = _pac_text(item.get("connections"), language)
|
||
if language == "zh":
|
||
return f"行运{mover} → 本命{target}" + (f"({pac})" if pac else "")
|
||
return f"Transit {mover} to natal {target}" + (f" ({pac})" if pac else "")
|
||
if kind == "p8":
|
||
match = _P8.match(str(item or ""))
|
||
if not match or not _is_planet(match.group(1)) or match.group(2) not in SIGNS:
|
||
return None
|
||
mover = planet_name(match.group(1), language)
|
||
sign = sign_name(match.group(2), language)
|
||
if language == "zh":
|
||
return f"行运{mover}在{sign}," + ("落第七宫" if match.group(3) == "7H" else "落上升宫")
|
||
return f"Transit {mover} in {sign}, " + ("in the 7th house" if match.group(3) == "7H" else "in the ascendant house")
|
||
if kind == "parivartana":
|
||
found = _SWAP.findall(str(item or ""))
|
||
if not found or not all(_is_planet(planet) and sign in SIGNS for planet, sign in found):
|
||
return None
|
||
joiner = ";" if language == "zh" else "; "
|
||
partial = str(item).startswith("部分")
|
||
if language == "zh":
|
||
text = joiner.join(f"行运{planet_name(planet, language)}在{sign_name(sign, language)}" for planet, sign in found)
|
||
return ("单边:" if partial else "") + text
|
||
text = joiner.join(f"Transit {planet_name(planet, language)} in {sign_name(sign, language)}" for planet, sign in found)
|
||
return ("One side only: " if partial else "") + text
|
||
return None
|
||
|
||
|
||
_LL7L_CHECKS = (
|
||
("p5", ("Transit ascendant lord and 7th lord PAC", "行运上升主与七宫主 PAC 连接")),
|
||
("p8", ("Ascendant lord through the 7th, or 7th lord through the ascendant", "上升主过七宫或七宫主过上升")),
|
||
("parivartana", ("Sign exchange", "互换星座")),
|
||
)
|
||
|
||
|
||
def _lord_transit(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("timing_and_predictive_systems", "transit_ll7l"))
|
||
if not isinstance(module, dict) or not module:
|
||
return _blank(language, ["item", "value"])
|
||
lead = _items_table([
|
||
(("Reference date", "参考日"), module.get("reference_date") or module.get("transit_date"), "text"),
|
||
(("Ascendant lord", "上升主"), module.get("lagna_lord"), "planet"),
|
||
(("7th lord", "七宫主"), module.get("seventh_lord"), "planet"),
|
||
], language)
|
||
rows = []
|
||
for key, (label_en, label_zh) in _LL7L_CHECKS:
|
||
check = module.get(key) if isinstance(module.get(key), dict) else {}
|
||
details = [_ll7l_detail(key, item, language) for item in check.get("details") or []]
|
||
details = [item for item in details if item]
|
||
hit = check.get("hit")
|
||
rows.append([
|
||
label_zh if language == "zh" else label_en,
|
||
show(hit, language) if isinstance(hit, bool) else undetermined(language),
|
||
(";" if language == "zh" else "; ").join(details) if details else ("无" if language == "zh" else "None"),
|
||
])
|
||
table = _md_table([lab("item", language), lab("detected", language), lab("value", language)], rows)
|
||
return lead + "\n\n" + table
|
||
|
||
|
||
_KP_VALUES = {
|
||
"placidus": ("Placidus", "Placidus"),
|
||
"whole_sign": ("Whole sign", "整宫制"),
|
||
"swiss_ephemeris_bhava_chalit": ("Swiss Ephemeris cusps", "Swiss Ephemeris 宫头"),
|
||
"explicit_cusps": ("Explicit cusps", "按宫头"),
|
||
"cusp_to_cusp": ("Cusp to cusp", "宫头到宫头"),
|
||
"candidate_window_only": ("Candidate windows only", "只给候选窗口"),
|
||
}
|
||
_KP_RULING = (
|
||
("day_lord", ("Day lord", "日主")),
|
||
("moon_sign_lord", ("Moon sign lord", "月亮星座主")),
|
||
("moon_star_lord", ("Moon star lord", "月亮星宿主")),
|
||
("moon_sub_lord", ("Moon sub lord", "月亮 Sub 主")),
|
||
("ascendant_sign_lord", ("Ascendant sign lord", "上升星座主")),
|
||
("ascendant_star_lord", ("Ascendant star lord", "上升星宿主")),
|
||
("ascendant_sub_lord", ("Ascendant sub lord", "上升 Sub 主")),
|
||
)
|
||
|
||
|
||
def _kp(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "kp")) or {}
|
||
if not isinstance(module, dict) or not module:
|
||
return _blank(language, ["item", "value"])
|
||
profile = module.get("kp_profile") if isinstance(module.get("kp_profile"), dict) else {}
|
||
settings = module.get("calculation_profile") if isinstance(module.get("calculation_profile"), dict) else {}
|
||
ayanamsa = profile.get("ayanamsa_display")
|
||
if not isinstance(ayanamsa, str) or "_" in ayanamsa:
|
||
ayanamsa = None
|
||
requested = profile.get("requested_report_ayanamsa")
|
||
table = _items_table([
|
||
(("KP ayanamsa", "KP 岁差"), ayanamsa, "text"),
|
||
(("Main report ayanamsa", "主报告岁差"), requested.title() if isinstance(requested, str) and requested.isalpha() else None, "text"),
|
||
(("House system", "宫制"), _pair(_KP_VALUES, profile.get("house_system"), language), "text"),
|
||
(("Cusp source", "宫头来源"), _pair(_KP_VALUES, profile.get("cusp_source"), language), "text"),
|
||
(("House basis", "宫位依据"), _pair(_KP_VALUES, module.get("house_basis"), language), "text"),
|
||
(("Occupancy basis", "落宫依据"), _pair(_KP_VALUES, module.get("house_occupancy_basis"), language), "text"),
|
||
(("Node mode", "交点模式"), settings.get("node_mode"), "text"),
|
||
(("Timing policy", "应期口径"), _pair(_KP_VALUES, profile.get("timing_policy"), language), "text"),
|
||
(("Status", "状态"), profile.get("status"), "text"),
|
||
], language)
|
||
ruling = module.get("ruling_planets") if isinstance(module.get("ruling_planets"), dict) else {}
|
||
rows = []
|
||
for key, (label_en, label_zh) in _KP_RULING:
|
||
if _is_planet(ruling.get(key)):
|
||
rows.append([label_zh if language == "zh" else label_en, show(ruling.get(key), language, "planet")])
|
||
if not rows:
|
||
return table
|
||
return table + "\n\n" + _md_table([lab("role", language), lab("planet", language)], rows)
|
||
|
||
|
||
_KARAKA_ROLES = (
|
||
("Atmakaraka", ("Atmakaraka (AK)", "灵魂指示星(AK)")),
|
||
("Amatyakaraka", ("Amatyakaraka (AmK)", "事业指示星(AmK)")),
|
||
("Bhratrikaraka", ("Bhratrikaraka (BK)", "兄弟指示星(BK)")),
|
||
("Matrikaraka", ("Matrikaraka (MK)", "母亲指示星(MK)")),
|
||
("Pitrukaraka", ("Pitrukaraka (PiK)", "父亲指示星(PiK)")),
|
||
("Putrakaraka", ("Putrakaraka (PK)", "子女指示星(PK)")),
|
||
("Gnatikaraka", ("Gnatikaraka (GK)", "障碍指示星(GK)")),
|
||
("Darakaraka", ("Darakaraka (DK)", "配偶指示星(DK)")),
|
||
)
|
||
|
||
|
||
def _jaimini_details(packet: dict, language: str) -> str:
|
||
jaimini = _first(packet, ("advanced_systems", "jaimini")) or {}
|
||
if not isinstance(jaimini, dict):
|
||
return _blank(language, ["karaka", "planet"])
|
||
seven_root = jaimini.get("chara_karaka_7") if isinstance(jaimini.get("chara_karaka_7"), dict) else {}
|
||
eight_root = jaimini.get("chara_karaka_8") if isinstance(jaimini.get("chara_karaka_8"), dict) else {}
|
||
seven = seven_root.get("karaka_table") if isinstance(seven_root.get("karaka_table"), dict) else {}
|
||
eight = eight_root.get("karaka_table_8") if isinstance(eight_root.get("karaka_table_8"), dict) else {}
|
||
rows = []
|
||
for key, (label_en, label_zh) in _KARAKA_ROLES:
|
||
one = seven.get(key) if isinstance(seven.get(key), dict) else {}
|
||
two = eight.get(key) if isinstance(eight.get(key), dict) else {}
|
||
if not one and not two:
|
||
continue
|
||
reversed_flag = two.get("rahu_reverse_applied")
|
||
rows.append({
|
||
"karaka": label_zh if language == "zh" else label_en,
|
||
"seven_planet": one.get("planet"),
|
||
"seven_degree": one.get("degree_in_sign"),
|
||
"eight_planet": two.get("planet"),
|
||
"eight_degree": two.get("effective_degree_in_sign"),
|
||
"rahu_reversed": reversed_flag if isinstance(reversed_flag, bool) else None,
|
||
})
|
||
note = (
|
||
"七曜口径不计罗睺;八曜口径计入罗睺,罗睺按 30° 减去度数排序。两种排序分开看,不混排。"
|
||
if language == "zh"
|
||
else "The seven-karaka scheme leaves Rahu out. The eight-karaka scheme counts Rahu by 30 degrees minus its degree. Read the two rankings separately."
|
||
)
|
||
table = _rows_from(rows, language, [
|
||
("karaka", "text"), ("seven_planet", "planet"), ("seven_degree", "text"),
|
||
("eight_planet", "planet"), ("eight_degree", "text"), ("rahu_reversed", "text"),
|
||
])
|
||
return note + "\n\n" + table
|
||
|
||
|
||
_TARA_COMBINED = {
|
||
"double_auspicious": ("Both favourable", "双吉"),
|
||
"double_dangerous": ("Both unfavourable", "双凶"),
|
||
"tara_good_chandra_bad": ("Star favourable, sign unfavourable", "星吉宫凶"),
|
||
"tara_bad_chandra_good": ("Star unfavourable, sign favourable", "星凶宫吉"),
|
||
"mixed_favorable": ("Leaning favourable", "偏吉"),
|
||
"mixed_unfavorable": ("Leaning unfavourable", "偏凶"),
|
||
"neutral": ("Neutral", "中性"),
|
||
}
|
||
_QUALITY = {
|
||
"auspicious": ("Favourable", "吉"),
|
||
"dangerous": ("Unfavourable", "凶"),
|
||
"neutral": ("Neutral", "中性"),
|
||
}
|
||
|
||
|
||
def _tara_name(entry: dict, key_en: str, key_zh: str, language: str) -> str | None:
|
||
if language == "zh":
|
||
value = entry.get(key_zh)
|
||
return value if isinstance(value, str) and value.strip() else None
|
||
value = entry.get(key_en)
|
||
if not isinstance(value, str):
|
||
return None
|
||
head = value.split("(", 1)[0].strip()
|
||
return head or None
|
||
|
||
|
||
def _yogi_avayogi(packet: dict, language: str) -> str:
|
||
jaimini = _first(packet, ("advanced_systems", "jaimini")) or {}
|
||
local = jaimini.get("yogi_avayogi_local") if isinstance(jaimini, dict) else None
|
||
if not isinstance(local, dict) or local.get("status") == "blocked":
|
||
return _blank(language, ["point", "sign", "degree", "nakshatra", "lord", "house"])
|
||
rows = []
|
||
for key, label in (("yogi", "Yogi"), ("ava_yogi", "Ava Yogi")):
|
||
point = local.get(key) if isinstance(local.get(key), dict) else None
|
||
if not point:
|
||
continue
|
||
rows.append({
|
||
"point": label,
|
||
"sign": point.get("sign"),
|
||
"degree": point.get("degree_in_sign"),
|
||
"nakshatra": point.get("nakshatra"),
|
||
"lord": point.get("nakshatra_lord"),
|
||
"house": point.get("house"),
|
||
})
|
||
if not rows:
|
||
return _blank(language, ["point", "sign", "degree", "nakshatra", "lord", "house"])
|
||
note = (
|
||
"由本命太阳、月亮黄经按 93°20′ 与 186°40′ 算出,只给坐标,不单独下判断。"
|
||
if language == "zh"
|
||
else "Computed from the natal Sun and Moon by 93°20' and 186°40'. Coordinates only; not a judgment by itself."
|
||
)
|
||
return note + "\n\n" + _rows_from(rows, language, [
|
||
("point", "text"), ("sign", "sign"), ("degree", "text"),
|
||
("nakshatra", "text"), ("lord", "planet"), ("house", "text"),
|
||
])
|
||
|
||
|
||
def _tara(packet: dict, language: str) -> str:
|
||
module = _first(packet, ("advanced_systems", "nakshatra_adv")) or {}
|
||
combined = module.get("tara_chandra_combined") if isinstance(module, dict) else None
|
||
if not isinstance(combined, dict):
|
||
return _blank(language, ["planet", "tara"])
|
||
rows = []
|
||
for planet in _PLANETS:
|
||
item = combined.get(planet)
|
||
if not isinstance(item, dict):
|
||
continue
|
||
tara = item.get("tara") if isinstance(item.get("tara"), dict) else {}
|
||
chandra = item.get("chandra") if isinstance(item.get("chandra"), dict) else {}
|
||
offset = chandra.get("rashi_from_moon")
|
||
rows.append({
|
||
"planet": planet,
|
||
"nakshatra": item.get("nakshatra"),
|
||
"tara_index": tara.get("tara_index") + 1 if isinstance(tara.get("tara_index"), int) and not isinstance(tara.get("tara_index"), bool) else None,
|
||
"tara": _tara_name(tara, "tara_name", "tara_cn", language),
|
||
"tara_quality": _pair(_QUALITY, tara.get("quality"), language),
|
||
"chandra_offset": offset + 1 if isinstance(offset, int) and not isinstance(offset, bool) else None,
|
||
"chandra": _tara_name(chandra, "chandra_name", "chandra_cn", language),
|
||
"chandra_quality": _pair(_QUALITY, chandra.get("quality"), language),
|
||
"combined": _pair(_TARA_COMBINED, item.get("combined_score"), language),
|
||
})
|
||
note = (
|
||
"相对本命月亮的分类,不是当前行运。Tara 从月亮星宿起数,Chandra 位从月亮星座起数,起点都记为 1。"
|
||
if language == "zh"
|
||
else "Classes relative to the natal Moon, not current transits. Tara counts from the Moon's nakshatra and the Chandra place from the Moon's sign; both start at 1."
|
||
)
|
||
return note + "\n\n" + _rows_from(rows, language, [
|
||
("planet", "planet"), ("nakshatra", "text"), ("tara_index", "text"), ("tara", "text"),
|
||
("tara_quality", "text"), ("chandra_offset", "text"), ("chandra", "text"),
|
||
("chandra_quality", "text"), ("combined", "text"),
|
||
])
|
||
|
||
|
||
_PAKSHA = {"Shukla": ("Shukla (waxing)", "白分(上半月)"), "Krishna": ("Krishna (waning)", "黑分(下半月)")}
|
||
_WEEKDAY = {
|
||
"Sunday": ("Sunday", "星期日"), "Monday": ("Monday", "星期一"), "Tuesday": ("Tuesday", "星期二"),
|
||
"Wednesday": ("Wednesday", "星期三"), "Thursday": ("Thursday", "星期四"), "Friday": ("Friday", "星期五"),
|
||
"Saturday": ("Saturday", "星期六"),
|
||
}
|
||
_SUBHA = {
|
||
"subha": ("Auspicious", "吉"),
|
||
"asubha": ("Inauspicious", "凶"),
|
||
"mishra": ("Mixed", "中"),
|
||
"mixed": ("Mixed", "中"),
|
||
"neutral": ("Neutral", "中性"),
|
||
}
|
||
|
||
|
||
def _panchanga_raw(packet: dict) -> dict:
|
||
module = _first(packet, ("birth_and_parameters", "panchanga")) or _first(packet, ("advanced_systems", "panchanga"))
|
||
raw = module.get("raw") if isinstance(module, dict) and isinstance(module.get("raw"), dict) else module
|
||
return raw if isinstance(raw, dict) else {}
|
||
|
||
|
||
def _tithi(packet: dict, language: str) -> str:
|
||
tithi = _panchanga_raw(packet).get("tithi")
|
||
if not isinstance(tithi, dict):
|
||
return _blank(language, ["item", "value"])
|
||
table = _items_table([
|
||
(("Tithi", "Tithi"), tithi.get("full_name") or tithi.get("name"), "text"),
|
||
(("Paksha", "月相半月"), _pair(_PAKSHA, tithi.get("paksha"), language), "text"),
|
||
(("Tithi in paksha", "半月内序号"), tithi.get("tithi_in_paksha"), "text"),
|
||
(("Tithi number (1-30)", "Tithi 序号(1–30)"), tithi.get("tithi_num"), "text"),
|
||
(("Moon minus Sun", "月日黄经差"), tithi.get("moon_sun_diff"), "text"),
|
||
], language)
|
||
note = (
|
||
"Tithi 主星模块没有进公开报告资料包,这里只列出生 Tithi 本身,不另推主星。"
|
||
if language == "zh"
|
||
else "The Tithi-lord module is not in the public report packet. This section lists the birth Tithi only and does not derive its lord."
|
||
)
|
||
return table + "\n\n" + note
|
||
|
||
|
||
def _panchanga(packet: dict, language: str) -> str:
|
||
raw = _panchanga_raw(packet)
|
||
if not raw:
|
||
return _blank(language, ["item", "value", "status"])
|
||
|
||
def part(key: str) -> dict:
|
||
value = raw.get(key)
|
||
return value if isinstance(value, dict) else {}
|
||
|
||
tithi, nakshatra, yoga, karana, vara, hora = (part(key) for key in ("tithi", "nakshatra", "yoga", "karana", "vara", "hora"))
|
||
pada = nakshatra.get("pada")
|
||
star = nakshatra.get("nakshatra")
|
||
if isinstance(star, str) and pada is not None:
|
||
star = f"{star} {pada}"
|
||
weekday = _pair(_WEEKDAY, vara.get("vara"), language)
|
||
rows = [
|
||
(("Tithi", "Tithi"), tithi.get("full_name") or tithi.get("name"), tithi.get("quality")),
|
||
(("Nakshatra", "星宿"), star, nakshatra.get("quality")),
|
||
(("Yoga", "Yoga"), yoga.get("yoga"), yoga.get("quality")),
|
||
(("Karana", "Karana"), karana.get("karana"), karana.get("quality")),
|
||
(("Weekday", "星期"), weekday, vara.get("quality")),
|
||
(("Weekday lord", "星期主星"), planet_name(vara.get("vara_lord"), language) if _is_planet(vara.get("vara_lord")) else None, None),
|
||
(("Hora lord", "Hora 主星"), planet_name(hora.get("hora_lord"), language) if _is_planet(hora.get("hora_lord")) else None, hora.get("quality")),
|
||
]
|
||
body = []
|
||
for (label_en, label_zh), value, quality in rows:
|
||
body.append([
|
||
label_zh if language == "zh" else label_en,
|
||
show(value, language),
|
||
_pair(_SUBHA, quality, language) or ("—" if quality is None else undetermined(language)),
|
||
])
|
||
count = raw.get("auspicious_count")
|
||
total = raw.get("total_elements")
|
||
if isinstance(count, int) and isinstance(total, int):
|
||
score = raw.get("overall_score")
|
||
value = f"{count} / {total}"
|
||
if isinstance(score, (int, float)) and not isinstance(score, bool):
|
||
value += f"({show(score, language)})" if language == "zh" else f" ({show(score, language)})"
|
||
body.append(["吉性要素" if language == "zh" else "Auspicious elements", value, "—"])
|
||
return _md_table([lab("item", language), lab("value", language), lab("quality", language)], body)
|
||
|
||
|
||
_BUILDERS = {
|
||
"Vimshottari Birth-Balance": _vimshottari,
|
||
"Kalachakra Dasha": _kalachakra,
|
||
"Yogini Periods": _yogini,
|
||
"Tribhagi Profile": _tribhagi_profile,
|
||
"Tribhagi Periods": _tribhagi_periods,
|
||
"Navamsha AD Duration": _navamsha,
|
||
"Nakshatra Dasha Context": _nakshatra_context,
|
||
"Bhrigu Cycle": _bhrigu_cycle,
|
||
"Bhrigu Approximate Progression": _bhrigu_sequence,
|
||
"Conditional Dasha Applicability": _applicability,
|
||
"Dasha Boundary Scan": _sandhi,
|
||
"Current Dasha Convergence": _convergence,
|
||
"Chara Dasha Period Durations": _chara,
|
||
"Narayana Method Comparison": _narayana,
|
||
"Annual Return House Framework": _annual_houses,
|
||
"Annual Return Calculation Contract": _annual,
|
||
"Tajika Aspect and Yoga Candidates": _tajika_candidates,
|
||
"Tripataki Chakra Detail": _tripataki,
|
||
"Annual Tajika Strength": _tajika_strength,
|
||
"Annual Saham Formula Trace": _annual_saham_trace,
|
||
}
|
||
|
||
|
||
def _chapter_sections() -> list[list[str]]:
|
||
order = [title for title, _zh in SECTIONS]
|
||
groups = [
|
||
order[:14],
|
||
order[14:20],
|
||
order[20:35],
|
||
order[35:46],
|
||
order[46:63],
|
||
order[63:],
|
||
]
|
||
return groups
|
||
|
||
|
||
def _render_named(packet: dict, language: str, title: str) -> str:
|
||
builder = _BUILDERS.get(title)
|
||
if builder:
|
||
return builder(packet, language)
|
||
extra = {
|
||
"Shadbala Subcomponent": lambda: _shadbala_parts(packet, language, False),
|
||
"Shadbala Result": lambda: _shadbala_parts(packet, language, True),
|
||
"House Components and Physical Coordinates": lambda: _coordinates(packet, language),
|
||
"Bhava Bala Three-Component": lambda: _bhava(packet, language),
|
||
"Vimsopaka Contribution": lambda: _vimsopaka(packet, language),
|
||
"AV Contribution and Reduction": lambda: _av_reduction(packet, language),
|
||
"AV Yoga Pinda": lambda: _av_pinda(packet, language),
|
||
"AV Sign Assessments": lambda: _av_signs(packet, language),
|
||
"Prastara Totals": lambda: _prastara(packet, language),
|
||
"Avastha Inputs": lambda: _avastha(packet, language),
|
||
"D30 States": lambda: _d30(packet, language, False),
|
||
"D30 Source Input": lambda: _d30(packet, language, True),
|
||
"Pushkara Basis": lambda: _pushkara(packet, language),
|
||
"Lagna Vargottamamsa": lambda: _lagna_vargottama(packet, language),
|
||
"Goel Jaimini Strength and Special Points": lambda: _goel(packet, language),
|
||
"Neutral and Compound Relationships": lambda: _friendship(packet, language),
|
||
"Relationship Conventions": lambda: _relationship_note(packet, language),
|
||
"Dispositor Terminals": lambda: _dispositor(packet, language, True),
|
||
"Dispositor Chain": lambda: _dispositor(packet, language, False),
|
||
"Aspect Geometry": lambda: _aspects(packet, language),
|
||
"Argala": lambda: _argala(packet, language),
|
||
"Congregation": lambda: _congregation(packet, language),
|
||
"Bhrigu Relationship Geometry": lambda: _geometry(packet, language),
|
||
"Yoga Participants": lambda: _yoga_participants(packet, language),
|
||
"Canonical Yoga Rows": lambda: _canonical_yogas(packet, language),
|
||
"Whole-Sign Occupants": lambda: _occupants(packet, language),
|
||
"Nakshatra Reference": lambda: _nakshatra_reference(packet, language),
|
||
"Tara-Chandra Matrix": lambda: _tara(packet, language),
|
||
"Natal Tithi Lord": lambda: _tithi(packet, language),
|
||
"Birth Calendar": lambda: _panchanga(packet, language),
|
||
"Pancha Pakshi": lambda: _missing_module(language, ["name", "value", "status"]),
|
||
"Natal Vivah Saham": lambda: _vivah(packet, language),
|
||
"Natal Saham Formula Basis": lambda: _saham_basis(packet, language),
|
||
"Natal Saham Formula Trace": lambda: _natal_saham_trace(packet, language),
|
||
"Arudha / UL / A10": lambda: _special_points(packet, language),
|
||
"Jaimini Calculation Details": lambda: _jaimini_details(packet, language),
|
||
"Graha Pada": lambda: _graha_pada(packet, language),
|
||
"Jaimini Yogi and Ava Yogi": lambda: _yogi_avayogi(packet, language),
|
||
"Karakamsha": lambda: _one_point(packet, language, "karakamsha"),
|
||
"Darakaraka": lambda: _one_point(packet, language, "darakaraka"),
|
||
"Rashi Tulya Navamsa": lambda: _rashi_tulya(packet, language),
|
||
"Moon Chart Reference": lambda: _moon(packet, language),
|
||
"Sudarshana Points": lambda: _sudarshana(packet, language),
|
||
"Dated Transit": lambda: _transit(packet, language),
|
||
"Multi-Reference Transit": lambda: _multi_transit(packet, language),
|
||
"Double Transit PAC": lambda: _double_transit(packet, language),
|
||
"Lagna / 7th Lord Transit": lambda: _lord_transit(packet, language),
|
||
"Natal Calculation Conventions": lambda: _settings(packet, language),
|
||
"KP Calculation Conventions": lambda: _kp(packet, language),
|
||
"D9 Dignity": lambda: _d9(packet, language),
|
||
"Divisional Mapping": lambda: _mapping(packet, language),
|
||
"Bhava Chalit Planet Placements": lambda: _chalit_planets(packet, language),
|
||
"Bhava Chalit Centres": lambda: _chalit_centres(packet, language),
|
||
"Bhava Chalit Convention": lambda: _chalit_convention(packet, language),
|
||
}
|
||
renderer = extra.get(title)
|
||
if renderer is None:
|
||
raise KeyError(title)
|
||
return renderer()
|
||
|
||
|
||
def render_parity_chapters(packet: dict, language: str = "zh") -> str:
|
||
"""Append-only chapters. language is ``zh`` or ``en``."""
|
||
if language not in {"zh", "en"}:
|
||
raise ValueError(language)
|
||
packet = packet if isinstance(packet, dict) else {}
|
||
blocks = []
|
||
for (chapter_en, chapter_zh), titles in zip(CHAPTERS, _chapter_sections()):
|
||
heading = chapter_zh if language == "zh" else chapter_en
|
||
parts = [f"## {heading}"]
|
||
for title in titles:
|
||
parts.append(_section(title, language, _render_named(packet, language, title)))
|
||
blocks.append("\n\n".join(parts))
|
||
return "\n\n".join(blocks).strip() + "\n"
|
||
|
||
|
||
def column_keys_used() -> list[str]:
|
||
return sorted(LABELS)
|