14b94be5ea
Retira el commit 2713e13, que tenía 3 causas raíz confirmadas por Opix y
Claudix (ver issue #20, comment-676 y siguientes): domingos de TO/Pascua
desplazados -1 semana, Cuaresma contada mal en bloques de 7 desde Ceniza, y
Adviento evaluado antes que Navidad (fuga el 26-31 de diciembre, rompe la
Sagrada Familia). Además special() daba precedencia a fiestas fijas sobre
domingos y hasta sobre la Pascua misma (2035: la Anunciación se comía el
Domingo de Resurrección).
Reescritura completa:
- Numeración de Tiempo Ordinario con omisión de semanas según la norma real
(Normas Universales sobre el Año Litúrgico, 1969): se cuenta hacia atrás
desde Cristo Rey = semana 34.
- Las 3 semanas tapadas por Pentecostés/Trinidad/Corpus llevan ferias de TO
en sus días laborables (el TO arranca el lunes después de Pentecostés).
- Ascensión y Corpus trasladados al domingo (práctica CEE); Bautismo del
Señor, Sagrada Familia, Cristo Rey y Ascensión tienen índice propio (antes
se disolvían en la etiqueta genérica del domingo vecino).
- 26 dic-Bautismo indexado por fecha fija (como el libro), no por día de la
semana.
- Precedencia real para las 11 solemnidades de fecha fija: ceden ante
domingos de Adviento/Cuaresma/Pascua, Ramos, lunes-jueves de Semana Santa,
Triduo, Ascensión y Pentecostés; entre solemnidades "del Señor" > "de un
Santo" (Corpus gana a San Juan Bautista en 2038, no al revés). Anunciación
con la regla de traslado específica documentada.
Cambio de diseño: cada fecha referencia directamente `book_index` (1-500)
sobre entradas-libro.json en vez de una clave de texto libre — evita que el
vocabulario del calendario diverja del contenido real del libro.
Test de aceptación (tests/test_liturgical_calendar_coverage.py): cobertura
bidireccional contra las 500 entradas + fechas móviles verificadas con un
cómputo de Pascua independiente. 497/500 entradas alcanzadas en 2026-2040;
las 3 restantes (34, 62, 134) están documentadas en el propio JSON como
aritmética real del rango, no fallo del generador.
Metodología de validación con fuentes no-CEE: horariosdemisa.com y
archisevilla.org (traslado de la Ascensión), remitly.com/aciprensa/nacion.com
(traslado de Corpus), catholicvirginian.org/diocesetucson (regla de traslado
de la Anunciación). Detalle completo en issue #20.
Co-authored-by: rafa <rcalvotorrejon@gmail.com>
156 lines
5.7 KiB
Python
156 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Test de aceptación del calendario litúrgico 2026-2040 (issue #20).
|
|
|
|
Criterio acordado entre Codix/Opix/Claudix: cobertura bidireccional contra
|
|
las 500 entradas del libro, más un puñado de fechas móviles verificadas de
|
|
forma independiente (cómputo de Pascua propio, no la fuente del generador).
|
|
|
|
Uso: python3 tests/test_liturgical_calendar_coverage.py
|
|
Sale con exit code != 0 si algo falla.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from datetime import date, timedelta
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CAL_PATH = ROOT / "data/evangelio-diario/calendario-liturgico-2026-2040.json"
|
|
BOOK_PATH = ROOT / "data/evangelio-diario/entradas-libro.json"
|
|
|
|
|
|
def easter_sunday(year: int) -> date:
|
|
a = year % 19
|
|
b = year // 100
|
|
c = year % 100
|
|
d_ = b // 4
|
|
e = b % 4
|
|
f = (b + 8) // 25
|
|
g = (b - f + 1) // 3
|
|
h = (19 * a + b - d_ - g + 15) % 30
|
|
i = c // 4
|
|
k = c % 4
|
|
l = (32 + 2 * e + 2 * i - h - k) % 7
|
|
m = (a + 11 * h + 22 * l) // 451
|
|
month = (h + l - 7 * m + 114) // 31
|
|
day = ((h + l - 7 * m + 114) % 31) + 1
|
|
return date(year, month, day)
|
|
|
|
|
|
def fail(msg: str, errors: list[str]) -> None:
|
|
errors.append(msg)
|
|
|
|
|
|
def main() -> int:
|
|
errors: list[str] = []
|
|
cal = json.loads(CAL_PATH.read_text(encoding="utf-8"))
|
|
book = json.loads(BOOK_PATH.read_text(encoding="utf-8"))
|
|
dates = cal["dates"]
|
|
n_book = len(book)
|
|
|
|
# 1) toda fecha del rango existe y tiene book_index valido
|
|
d = date.fromisoformat(cal["range"]["from"])
|
|
end = date.fromisoformat(cal["range"]["to"])
|
|
n_dates = 0
|
|
while d <= end:
|
|
iso = d.isoformat()
|
|
if iso not in dates:
|
|
fail(f"fecha ausente: {iso}", errors)
|
|
else:
|
|
idx = dates[iso]["book_index"]
|
|
if not (1 <= idx <= n_book):
|
|
fail(f"{iso}: book_index fuera de rango: {idx}", errors)
|
|
n_dates += 1
|
|
d += timedelta(days=1)
|
|
|
|
# 2) toda entrada del libro es alcanzable en algun año del rango
|
|
used = {v["book_index"] for v in dates.values()}
|
|
never_used = sorted(set(range(1, n_book + 1)) - used)
|
|
# las 3 excepciones conocidas y documentadas en el propio JSON (ver "notes")
|
|
known_gaps = {34, 62, 134}
|
|
unexpected_gaps = [i for i in never_used if i not in known_gaps]
|
|
if unexpected_gaps:
|
|
fail(f"entradas del libro nunca alcanzadas (no documentadas): {unexpected_gaps}", errors)
|
|
|
|
# 3) fechas moviles clave, computadas de forma independiente (no depende
|
|
# del propio generador ni de su fuente CEE)
|
|
checks_ok = 0
|
|
for year in range(2026, 2041):
|
|
easter = easter_sunday(year)
|
|
expects = [
|
|
(easter, 199, "Pascua"),
|
|
(easter - timedelta(days=46), 141, "Ceniza"),
|
|
(easter + timedelta(days=49), 258, "Pentecostes"),
|
|
]
|
|
for dt, expected_idx, label in expects:
|
|
v = dates.get(dt.isoformat())
|
|
got = v["book_index"] if v else None
|
|
if got != expected_idx:
|
|
fail(f"{year} {label} {dt}: esperado idx={expected_idx}, obtenido={got}", errors)
|
|
else:
|
|
checks_ok += 1
|
|
palm = easter - timedelta(days=7)
|
|
v = dates.get(palm.isoformat())
|
|
if not (v and 190 <= v["book_index"] <= 192):
|
|
fail(f"{year} Ramos {palm}: obtenido={v}", errors)
|
|
else:
|
|
checks_ok += 1
|
|
trinity = easter + timedelta(days=56)
|
|
v = dates.get(trinity.isoformat())
|
|
if not (v and 259 <= v["book_index"] <= 261):
|
|
fail(f"{year} Trinidad {trinity}: obtenido={v}", errors)
|
|
else:
|
|
checks_ok += 1
|
|
|
|
# 4) solemnidades con clave propia, todos los años (no diluidas en un
|
|
# domingo/feria generico) -- el bug central del commit retirado 2713e13
|
|
for year in range(2026, 2041):
|
|
ep = date(year, 1, 6)
|
|
baptism = (ep + timedelta(days=1)) if ep.weekday() == 6 else (
|
|
ep + timedelta(days=(6 - ep.weekday()) % 7)
|
|
)
|
|
v = dates.get(baptism.isoformat())
|
|
if not (v and 54 <= v["book_index"] <= 56):
|
|
fail(f"{year} Bautismo {baptism}: sin clave propia ({v})", errors)
|
|
|
|
easter = easter_sunday(year)
|
|
ascension = easter + timedelta(days=42)
|
|
v = dates.get(ascension.isoformat())
|
|
if not (v and 249 <= v["book_index"] <= 251):
|
|
fail(f"{year} Ascension {ascension}: sin clave propia ({v})", errors)
|
|
|
|
corpus = easter + timedelta(days=63)
|
|
v = dates.get(corpus.isoformat())
|
|
if not (v and 262 <= v["book_index"] <= 264):
|
|
fail(f"{year} Corpus {corpus}: sin clave propia o no en domingo ({v})", errors)
|
|
|
|
years_with_ck = {k[:4] for k, v in dates.items() if 481 <= v["book_index"] <= 483}
|
|
if len(years_with_ck) != 15:
|
|
fail(f"Cristo Rey ausente en algun año: {sorted(years_with_ck)}", errors)
|
|
|
|
years_with_family = {k[:4] for k, v in dates.items() if v["book_index"] == 40}
|
|
if len(years_with_family) != 15:
|
|
fail(f"Sagrada Familia ausente en algun año: {sorted(years_with_family)}", errors)
|
|
|
|
# 5) Adviento no debe seguir contando tras Navidad (fuga del commit retirado)
|
|
leak = [k for k, v in dates.items()
|
|
if k[5:7] == "12" and int(k[8:10]) >= 26 and v["book_index"] <= 37]
|
|
if leak:
|
|
fail(f"fuga de Adviento tras Navidad: {leak[:10]}", errors)
|
|
|
|
print(f"fechas verificadas: {n_dates}")
|
|
print(f"book_index distintos usados: {len(used)}/{n_book}")
|
|
print(f"chequeos de fechas moviles ok: {checks_ok}")
|
|
if errors:
|
|
print(f"\nFALLOS ({len(errors)}):")
|
|
for e in errors:
|
|
print(" -", e)
|
|
return 1
|
|
print("\nTodo OK.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|