hetchyy's picture
deploy: inspector prod @ b341525
48c91bc verified
Raw
History Blame Contribute Delete
2.88 kB
"""Mushaf coverage gaps for a recitation, in concise notation.
Given the verses a recitation actually covers, derive what's missing vs the full
mushaf and render it compactly for the release ``catalog.json`` + the changelog
table. Whole missing surahs and within-surah verse gaps are split into two
strings so even a partial recitation (e.g. a Juz' ʿAmma reciter) stays concise.
Shared by ``qua_jobs/cut_release.py`` for both the catalog field and the
changelog member rows, so the JSON and the human table can't drift.
"""
from __future__ import annotations
from collections.abc import Iterable
def verse_counts_from_surah_info(surah_info: dict) -> dict[int, int]:
"""``{surah: num_verses}`` from a loaded surah_info dict."""
return {int(s): int(info["num_verses"]) for s, info in surah_info.items()}
def _compress_ints(nums: Iterable[int]) -> str:
"""Collapse ints into comma-separated ascending runs: ``1-3,5,7-9``."""
ordered = sorted(set(nums))
if not ordered:
return ""
runs: list[str] = []
start = prev = ordered[0]
for n in ordered[1:]:
if n == prev + 1:
prev = n
continue
runs.append(f"{start}-{prev}" if start != prev else f"{start}")
start = prev = n
runs.append(f"{start}-{prev}" if start != prev else f"{start}")
return ",".join(runs)
def missing_coverage(
present: Iterable[tuple[int, int]], surah_verse_counts: dict[int, int]
) -> tuple[str, str]:
"""Return ``(missing_surahs, missing_verses)`` for a recitation.
``present`` is the ``(surah, ayah)`` the recitation covers;
``surah_verse_counts`` is ``{surah: num_verses}`` for the full mushaf.
- ``missing_surahs`` — surahs with ZERO covered verses, as compact ascending
runs of surah numbers (``"1-84"``, ``"4,7,9,37,39-40,45,65"``).
- ``missing_verses`` — within-surah gaps only (a surah that IS partly
covered), as ``surah:ayah`` runs, surahs joined by ``", "``
(``"7:116, 41:15"``, ``"2:30,32-34, 5:11"``, ``"75:18-40"``).
Both are ``""`` when nothing is missing in that category. A whole missing
surah never appears in ``missing_verses`` and vice-versa, so the two strings
partition the gap and each stays short.
"""
present_by_surah: dict[int, set[int]] = {}
for surah, ayah in present:
present_by_surah.setdefault(int(surah), set()).add(int(ayah))
whole_missing: list[int] = []
partial_parts: list[str] = []
for surah in sorted(surah_verse_counts):
covered = present_by_surah.get(surah, set())
if not covered:
whole_missing.append(surah)
continue
gaps = set(range(1, surah_verse_counts[surah] + 1)) - covered
if gaps:
partial_parts.append(f"{surah}:{_compress_ints(gaps)}")
return _compress_ints(whole_missing), ", ".join(partial_parts)