aboutsummaryrefslogtreecommitdiff
path: root/scripts/genlect-of-cr.py
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 09:30:22 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-28 09:30:22 +0200
commit6641ce335de522e6d51c47ad9d14f69726650592 (patch)
treeff56e36d12b46698bd41a26e650df99436ed3f22 /scripts/genlect-of-cr.py
parent70b805018f0ae0d8424c3c7de8f1169476385a35 (diff)
downloadlectio-6641ce335de522e6d51c47ad9d14f69726650592.tar.gz
lectio-6641ce335de522e6d51c47ad9d14f69726650592.zip
fix(of): canonical OT ferial week numbering + CR-sourced ferial lectionary
The Ordinary Form ferial lectionary had two linked defects, both from harvesting niedziela.pl by date: (1) post-Pentecost Ordinary-Time ferial WEEK NUMBERS were one too high (temporal.go counted back from Christ the King using the weekday itself, not its preceding Sunday, so a ferial took the next week's number), and (2) ~17 ferial keys held a SAINT's readings (stored when a saint displaced the ferial on the queried date, e.g. week 22 smeared with a Marian Prov 8/John 2), plus 16 weekday slots were missing entirely. - temporal.go: number a span-2 ferial from its preceding Sunday. Verified 388/388 ferial-week agreement with calapi across 2026-2028. - scripts/genlect-of-cr.py: regenerate the Ordinary-Time ferial lectionary (first/psalm/gospel, both years, all 34 weeks) from catholic-resources.org (Fr. Felix Just, S.J.), which is keyed by liturgical position (immune to by-date contamination) and complete. Citations only; text still rendered from the public-domain corpora. of-lectionary.ini OT ferials now 408 (was 392). Fixes ~17 contaminated entries + 16 gaps + the week-numbering audit item. Psalms already share modern numbering, so no conversion. Adds TestOFOrdinaryTimeFerialWeek. Credits catholic-resources.org in NOTICE. Bumps to 0.40.0.
Diffstat (limited to 'scripts/genlect-of-cr.py')
-rw-r--r--scripts/genlect-of-cr.py119
1 files changed, 119 insertions, 0 deletions
diff --git a/scripts/genlect-of-cr.py b/scripts/genlect-of-cr.py
new file mode 100644
index 0000000..468a85d
--- /dev/null
+++ b/scripts/genlect-of-cr.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+"""Regenerate the Ordinary Form WEEKDAY (ferial) lectionary for Ordinary Time
+from catholic-resources.org (Fr. Felix Just, S.J.), replacing the entries that
+were harvested from niedziela.pl.
+
+Why: the niedziela harvest is by-date, so it (a) stores a saint's readings under
+a ferial key whenever a saint displaced the ferial on the queried date, and
+(b) inherits lectio's old Ordinary-Time week-number off-by-one. catholic-
+resources.org is keyed by liturgical POSITION (week + weekday + year), immune to
+both, complete (all 34 weeks x both years), and gives citations only (no text),
+which matches lectio's licensing. Scripture citations only; see NOTICE.
+
+Requires network. From the repo root: python3 scripts/genlect-of-cr.py
+Rewrites the ordinary-<week>-<day>-<I|II> entries in
+internal/caldata/of-lectionary.ini, keyed by the canonical week number (the same
+number temporalEF/temporal now computes).
+"""
+import re, sys, urllib.request
+
+INI = "internal/caldata/of-lectionary.ini"
+SRC = {"I": "https://www.catholic-resources.org/Lectionary/2002USL-Weekdays-OT-I.htm",
+ "II": "https://www.catholic-resources.org/Lectionary/2002USL-Weekdays-OT-II.htm"}
+DAYMAP = {"mon": "mon", "tues": "tue", "tue": "tue", "wed": "wed",
+ "thurs": "thu", "thu": "thu", "fri": "fri", "sat": "sat"}
+
+# CR abbreviation -> lectio full book name (matches the existing file's style).
+BOOK = {}
+def reg(full, *aliases):
+ for a in aliases:
+ BOOK[a.lower().replace(".", "").strip()] = full
+reg("Genesis","gen");reg("Exodus","exod","ex");reg("Leviticus","lev");reg("Numbers","num")
+reg("Deuteronomy","deut");reg("Joshua","josh");reg("Judges","judg");reg("Ruth","ruth")
+reg("1 Samuel","1 sam");reg("2 Samuel","2 sam");reg("1 Kings","1 kgs");reg("2 Kings","2 kgs")
+reg("1 Chronicles","1 chr");reg("2 Chronicles","2 chr");reg("Ezra","ezra");reg("Nehemiah","neh")
+reg("Tobit","tob");reg("Judith","jdt");reg("Esther","esth");reg("1 Maccabees","1 macc");reg("2 Maccabees","2 macc")
+reg("Job","job");reg("Psalms","ps","pss");reg("Proverbs","prov");reg("Ecclesiastes","eccl")
+reg("Song of Solomon","song");reg("Wisdom","wis");reg("Sirach","sir")
+reg("Isaiah","isa");reg("Jeremiah","jer");reg("Lamentations","lam");reg("Baruch","bar")
+reg("Ezekiel","ezek");reg("Daniel","dan");reg("Hosea","hos");reg("Joel","joel");reg("Amos","amos")
+reg("Obadiah","obad");reg("Jonah","jonah");reg("Micah","mic");reg("Nahum","nah");reg("Habakkuk","hab")
+reg("Zephaniah","zeph");reg("Haggai","hag");reg("Zechariah","zech");reg("Malachi","mal")
+reg("Matthew","matt","mat");reg("Mark","mark");reg("Luke","luke");reg("John","john")
+reg("The Acts","acts");reg("Romans","rom");reg("1 Corinthians","1 cor");reg("2 Corinthians","2 cor")
+reg("Galatians","gal");reg("Ephesians","eph");reg("Philippians","phil");reg("Colossians","col")
+reg("1 Thessalonians","1 thess");reg("2 Thessalonians","2 thess");reg("1 Timothy","1 tim");reg("2 Timothy","2 tim")
+reg("Titus","titus");reg("Philemon","phlm");reg("Hebrews","heb");reg("James","jas")
+reg("1 Peter","1 pet");reg("2 Peter","2 pet");reg("1 John","1 john");reg("2 John","2 john");reg("3 John","3 john")
+reg("Jude","jude");reg("Revelation","rev")
+
+def norm_cite(c):
+ c = re.sub(r"^\s*See\s+", "", c.strip())
+ c = re.sub(r"[–—]|&mdash;|&ndash;", "-", c) # en/em dash -> hyphen
+ c = re.sub(r"\s+", " ", c).strip()
+ m = re.match(r"((?:[1-4]\s+)?[A-Za-z][A-Za-z]*\.?)\s*(.*)", c)
+ if not m:
+ return c
+ tok = m.group(1).lower().replace(".", "").strip()
+ full = BOOK.get(tok)
+ if not full:
+ sys.stderr.write(f" ! unknown book {m.group(1)!r} in {c!r}\n")
+ full = m.group(1)
+ verses = re.sub(r",\s+", ",", m.group(2)).replace(" ", "") # match file style: no spaces
+ return f"{full} {verses}".strip()
+
+def strip_tags(s):
+ return re.sub(r"<[^>]+>", "", s).replace("&nbsp;", " ").strip()
+
+def scrape(url, year):
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (lectio OF lectionary generator)"})
+ html = urllib.request.urlopen(req, timeout=40).read().decode("utf-8", "replace")
+ out = {}
+ for tr in re.split(r"<tr\b", html):
+ cells = [strip_tags(c) for c in re.findall(r"<td\b[^>]*>(.*?)</td>", tr, re.S)]
+ if len(cells) < 6:
+ continue
+ m = re.match(r"Week (\d+)\s*-\s*(Mon|Tues|Tue|Wed|Thurs|Thu|Fri|Sat)\b", cells[1])
+ if not m:
+ continue
+ wk, day = int(m.group(1)), DAYMAP[m.group(2).lower()]
+ out[(wk, day, year)] = {"first": norm_cite(cells[2]),
+ "psalm": norm_cite(cells[3]),
+ "gospel": norm_cite(cells[5])}
+ return out
+
+def main():
+ cr = {}
+ for yr, url in SRC.items():
+ d = scrape(url, yr)
+ sys.stderr.write(f"{url}: {len(d)} rows\n")
+ cr.update(d)
+
+ # parse existing file, keep header comment + all non-OT-ferial entries verbatim
+ text = open(INI).read()
+ head = text[:text.index("\n[")]
+ entries = {}
+ for block in re.split(r"\n(?=\[)", text[text.index("\n[") + 1:]):
+ m = re.match(r"\[(.+?)\]", block)
+ if m:
+ entries[m.group(1)] = block.rstrip("\n")
+ ot = re.compile(r"^ordinary-\d+-(mon|tue|wed|thu|fri|sat)-(I|II)$")
+ kept = {k: v for k, v in entries.items() if not ot.match(k)}
+
+ for (wk, day, yr), parts in cr.items():
+ key = f"ordinary-{wk}-{day}-{yr}"
+ lines = [f"[{key}]"]
+ for p in ("first", "psalm", "gospel"):
+ if parts[p]:
+ lines.append(f"{p} = {parts[p]}")
+ kept[key] = "\n".join(lines)
+
+ out = head.rstrip("\n") + "\n"
+ for k in sorted(kept):
+ out += "\n" + kept[k] + "\n"
+ open(INI, "w").write(out)
+ n = sum(1 for k in kept if k.startswith("ordinary-") and ot.match(k))
+ sys.stderr.write(f"wrote {len(kept)} entries ({n} OT ferials from catholic-resources.org)\n")
+
+if __name__ == "__main__":
+ main()