"""Blank report and chart columns (TASK-report-chart-blank-columns-20261003). BUG-1199 Kranti column with no producer; BUG-1200 Ashtakavarga house table labelled the with-Lagna total as SAV (split in the reader: ashtakavarga.py is in the frozen rectification identity and stays byte-identical); BUG-1201 ascendant without nakshatra; BUG-1203 the divisional-chart caveat moved to the chart section. Every chart here is fictional. """ from __future__ import annotations import copy import json import re from pathlib import Path import pytest from scripts.ashtakavarga import BAV_TOTALS, EXPECTED_SAV_TOTAL, SIGNS from scripts.jyotish_engine import NAKSHATRA_LIST, _nakshatra_fields, compute_chart_data from scripts.pl9_reader_export import _pl9_export_markdown_for_edition from tests.test_report_english_edition import CASES, _packet ROOT = Path(__file__).resolve().parents[1] CAVEAT_ZH = "分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。" def _independent_nakshatra(lon: float) -> tuple[str, int, str]: minutes = (lon % 360) * 60 index = int(minutes // 800) # 13°20′ = 800′ pada = int((minutes % 800) // 200) + 1 # 3°20′ = 200′ name, lord, _ = NAKSHATRA_LIST[index] return name, pada, lord @pytest.mark.parametrize("lon,expected", [ (0.0, ("Ashwini", 1, "Ketu")), (13.33, ("Ashwini", 4, "Ketu")), (13.34, ("Bharani", 1, "Venus")), (134.53, ("Purva Phalguni", 1, "Venus")), (149.3994, ("Uttara Phalguni", 1, "Sun")), (359.99, ("Revati", 4, "Mercury")), (360.0, ("Ashwini", 1, "Ketu")), ]) def test_nakshatra_fields_at_boundaries(lon, expected) -> None: fields = _nakshatra_fields(lon) assert (fields["nakshatra"], fields["nakshatra_pada"], fields["nakshatra_lord"]) == expected def test_chart_ascendant_carries_its_nakshatra_and_planets_are_unchanged() -> None: golden = json.loads((ROOT / "frontend/tests/fixtures/chart-view-golden.json").read_text(encoding="utf-8")) chart, *_ = compute_chart_data(1990, 6, 15, 12, 0, 39.9042, 116.4074, 8.0, ayanamsa_name="raman") asc = chart["ascendant"] assert (asc["nakshatra"], asc["nakshatra_pada"], asc["nakshatra_lord"]) == _independent_nakshatra(asc["lon"]) assert {k: asc[k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord")} == { k: golden["chart"]["ascendant"][k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord") } for name, planet in chart["planets"].items(): stored = golden["chart"]["planets"][name] assert (planet["nakshatra"], planet["nakshatra_pada"], planet["nakshatra_lord"]) == ( stored["nakshatra"], stored["nakshatra_pada"], stored["nakshatra_lord"]), name @pytest.fixture(scope="module") def editions() -> tuple[str, str, dict]: packet = _packet(CASES["day"]) zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data") english = copy.deepcopy(packet) english["report_language"] = "en" return zh, _pl9_export_markdown_for_edition(english, "full_data"), packet def _table_after(markdown: str, heading: str) -> list[list[str]]: lines = markdown.splitlines() start = next(i for i, line in enumerate(lines) if line.startswith("### ") and heading in line) rows = [] for line in lines[start + 1:]: if line.startswith("### ") or (rows and not line.startswith("|")): break if line.startswith("|") and not set(line) <= set("|- "): rows.append([cell.strip() for cell in line.strip("|").split("|")]) return rows def test_declination_table_has_no_kranti_column(editions) -> None: # 原值:两版标题都是 Declination / Speed,表头 Planet/Degree/Declination/Speed # 新值:中文标题「赤纬 / 速度」,表头 行星/度数/赤纬/速度;英文保持原文 # 原因:完整数据版中文清理翻译这四列。Kranti 列仍然不出现 for markdown, heading, header in ( (editions[0], "赤纬 / 速度", ["行星", "度数", "赤纬", "速度"]), (editions[1], "Declination / Speed", ["Planet", "Degree", "Declination", "Speed"]), ): assert "Kranti" not in markdown rows = _table_after(markdown, heading) assert rows[0] == header assert len(rows) == 8 and all(len(row) == 4 and "-" not in row for row in rows) def test_ashtakavarga_house_table_shows_three_real_columns(editions) -> None: zh, en, packet = editions expected_sav = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["sav"]["scores"] # BUG-1209 (乙8): a Sodhita SAV column comes first and the raw columns are # labelled Raw. Was: ["House", "Sign", "SAV", "Lagna BAV", "SAV + Lagna"] # with the raw cells at indexes 2..4 (now 3..5). sodhita = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["sodhita"]["sodhita_sav"]["scores"] # 原值:中文表头 House/Sign/…/上升 BAV;英文标题 Ashtakavarga Full House Scores,合计行 Total # 新值:中文按「完整宫位分数」找表,表头 宫位/星座/…/上升 BAV,合计行「合计」;英文标题 Full House Scores,表头 House/Sign/Lagna BAV,合计行 Total # 原因:完整数据版中文清理翻译 House/Sign/Lagna。英文先译整节标题,并把「合计」写成 Total,避免单字清理把标题拆碎或把合计留成汉字 for markdown, heading, header, total in ( (zh, "完整宫位分数", ["宫位", "星座", "Sodhita SAV", "Raw SAV", "上升 BAV", "Raw SAV + 上升"], "合计"), (en, "Full House Scores", ["House", "Sign", "Sodhita SAV", "Raw SAV", "Lagna BAV", "Raw SAV + Lagna"], "Total"), ): rows = _table_after(markdown, heading) assert rows[0] == header body, totals = rows[1:13], rows[13] for row in body: sav, lagna, full = (int(cell) for cell in row[3:]) assert sav + lagna == full assert int(row[2]) <= sav # 原值:合计行星座格是 "-" # 新值:星座格是 "-"、"未列" 或 "not listed";后面四格数字不变 # 原因:完整数据版清理器把空位写成「未列」。Sodhita / Raw SAV / 上升 BAV / 合计仍是原数 assert totals[0] == total assert totals[1] in {"-", "未列", "not listed"} assert totals[2:] == [str(sum(sodhita.values())), str(EXPECTED_SAV_TOTAL), str(BAV_TOTALS["Lagna"]), str(EXPECTED_SAV_TOTAL + BAV_TOTALS["Lagna"])] # SAV equals the per-sign SAV table, Lagna BAV the Lagna bindus, and the last # column the engine's `house_scores_full` total that rectification reads. ashtakavarga = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"] full = {row["sign"]: row["sav_score"] for row in ashtakavarga["house_scores_full"].values()} for row in _table_after(en, "Full House Scores")[1:13]: assert int(row[2]) == sodhita[row[1]] assert int(row[3]) == expected_sav[row[1]] assert int(row[4]) == ashtakavarga["bav"]["Lagna"]["bindus"][SIGNS.index(row[1])] assert int(row[5]) == full[row[1]] def test_ashtakavarga_house_table_never_labels_the_total_as_sav(editions) -> None: packet = copy.deepcopy(editions[2]) del packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["bav"]["Lagna"] rows = _table_after(_pl9_export_markdown_for_edition(packet, "full_data"), "Ashtakavarga 完整宫位分数") assert len(rows) == 13, "no total row without the split" for row in rows[1:]: # BUG-1209: raw SAV / Lagna BAV moved from [2:4] to [3:5]; Sodhita at [2]. assert row[2].isdigit() # 原值:缺数两格都是 "-" # 新值:缺数格是 "-" 或「未列」,且不是数字、也不标成 SAV # 原因:完整数据版清理器会把一部分空位写成「未列」 assert row[3] in {"-", "未列"} and row[4] in {"-", "未列"} assert not row[3].isdigit() and not row[4].isdigit() assert row[5].isdigit() def test_chart_section_carries_the_divisional_caveat(editions) -> None: zh, en, _ = editions assert re.search(r"### 本命与分盘北印度图盘\n\n" + re.escape(CAVEAT_ZH) + r"\n", zh) # 原值:### Natal and Divisional Charts (North Indian) 下一行是英文 caveat # 新值:### Natal and Divisional North Indian Charts;英文句或保留的中文 caveat 都算在 # 原因:完整数据版英文清理用上游标题。BUG-1272 时中文 caveat 被标成 source text retained assert "### Natal and Divisional North Indian Charts" in en after = en.split("### Natal and Divisional North Indian Charts", 1)[1][:800] assert "Divisional charts are sensitive to birth time" in after or "分盘对出生时间敏感" in after