aboutsummaryrefslogtreecommitdiff
path: root/scripts/genlect-of-season-ferials-cr.py
blob: 4cca8025cab18c65bf19862550fe6ca0995ddbcf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env python3
"""Re-source the Ordinary Form SEASONAL weekday (ferial) lectionary -- the proper
weekdays of Advent (weeks 1-3), Lent, and Eastertide -- from catholic-resources.org
(Fr. Felix Just, S.J.). These are single-cycle (the same every year), so both the
-I and -II weekday-cycle keys get identical readings.

Fixes niedziela-harvest defects found by diffing: contaminated Easter weekday
first readings (easter-3-mon had 1 Cor 15 instead of Acts 6:8-15), the missing
Easter-octave weekdays, and stray psalm-numbering artifacts. The DATE-based days
(late Advent Dec 17-24 and the Christmas weekdays) are handled elsewhere.

Scripture citations only; text rendered from the public-domain corpora. See NOTICE.
Requires network.  python3 scripts/genlect-of-season-ferials-cr.py
"""
import re, sys, html, urllib.request

INI = "internal/caldata/of-lectionary.ini"
PAGES = ["Weekdays-AdventChristmas", "Weekdays-Lent", "Weekdays-Easter"]
BASE = "https://www.catholic-resources.org/Lectionary/2002USL-{}.htm"
WD = {"m": "mon", "mon": "mon", "monday": "mon", "t": "tue", "tue": "tue", "tues": "tue", "tuesday": "tue",
      "w": "wed", "wed": "wed", "wednesday": "wed", "th": "thu", "thu": "thu", "thurs": "thu", "thursday": "thu",
      "f": "fri", "fri": "fri", "friday": "fri", "s": "sat", "sat": "sat", "saturday": "sat"}

BOOK = {}
def reg(full, *al):
    for a in al: 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","2 mac")
reg("Job","job");reg("Psalms","ps","pss");reg("Proverbs","prov");reg("Ecclesiastes","eccl")
reg("Song of Solomon","song","cant","songs");reg("Wisdom","wis");reg("Sirach","sir")
reg("Isaiah","isa","is");reg("Jeremiah","jer");reg("Lamentations","lam");reg("Baruch","bar")
reg("Ezekiel","ezek","ez");reg("Daniel","dan");reg("Hosea","hos");reg("Joel","joel");reg("Amos","amos")
reg("Obadiah","obad");reg("Jonah","jonah","jon");reg("Micah","mic","micah");reg("Nahum","nah");reg("Habakkuk","hab")
reg("Zephaniah","zeph");reg("Haggai","hag");reg("Zechariah","zech","zac");reg("Malachi","mal")
reg("Matthew","matt","mat","mt");reg("Mark","mark");reg("Luke","luke");reg("John","john","jn")
reg("The Acts","acts","act");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","hebr");reg("James","jas")
reg("1 Peter","1 pet","1 petr");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","apoc")
for _f in list(set(BOOK.values())): BOOK.setdefault(_f.lower(), _f)

def st(s):
    return html.unescape(re.sub(r"<[^>]+>", "", s)).replace("\xa0", " ").replace("\n", " ").replace("\r", " ").strip()

def clean_cite(raw):
    s = st(raw)
    s = re.sub(r"[†‡*]", "", s)            # dagger/star alternate-reading markers
    s = re.sub(r"[–—]", "-", s)
    s = re.sub(r"\([^)]*\)", "", s)
    s = re.sub(r"\s+and\s+.*", "", s)                # Ash Wed lists 2nd reading after "and" -> drop for first
    s = re.split(r"\s+or\b", s, maxsplit=1)[0]
    s = re.sub(r"^(cf\.?|see)\s+", "", s.strip(), flags=re.I).strip().strip(";,").strip()
    m = re.match(r"((?:[1-4]\s+)?[A-Za-z][A-Za-z]*)\.?\s*(.*)", s)
    if not m: return ""
    full = BOOK.get(m.group(1).lower().strip())
    if not full:
        sys.stderr.write(f"  ! unknown book {m.group(1)!r} in {raw!r}\n"); return ""
    return f"{full} {re.sub(r',\s+', ',', m.group(2)).replace(' ', '')}".strip()

def slug_of(day):
    d = re.sub(r"\s+", " ", st(day).lower())
    m = re.match(r"december (\d+)", d)                 # late Advent Dec 17-24: proper to the date
    if m and 17 <= int(m.group(1)) <= 24: return "advent-dec-" + m.group(1)
    m = re.match(r"dec\.? (\d+)", d)                   # Christmas octave ferials Dec 29-31
    if m and 29 <= int(m.group(1)) <= 31: return "christmas-dec-" + m.group(1)
    m = re.match(r"\[?\s*jan\.? (\d+)", d)             # Christmas weekdays before Epiphany, Jan 2-7
    if m and 2 <= int(m.group(1)) <= 7: return "christmas-jan-" + m.group(1)
    m = re.match(r"(monday|tuesday|wednesday|thursday|friday|saturday) after epiphany", d)
    if m: return "christmas-after-epiphany-" + WD[m.group(1)]
    if "ash wednesday" in d: return "lent-after-ashes-wed"
    m = re.match(r"(thursday|friday|saturday) after ash", d)
    if m: return "lent-after-ashes-" + WD[m.group(1)]
    m = re.search(r"octave of easter\s*[-–—]\s*(\w+)", d)
    if m and m.group(1) in WD: return "easter-octave-" + WD[m.group(1)]
    m = re.search(r"(\d+)\w* week of (advent|lent|easter)\s*[-–—]\s*(\w+)", d)
    if m and m.group(3) in WD: return f"{m.group(2)}-{m.group(1)}-" + WD[m.group(3)]
    return None   # date-based (Dec 17-24, Christmas weekdays): handled by the temporal engine, not here

def firstbook(c):
    m = re.match(r"((?:[1-4] )?[A-Za-z ]+?) (\d+)", c or "")
    return f"{m.group(1)} {m.group(2)}" if m else (c or "")

def main():
    cr = {}
    for p in PAGES:
        req = urllib.request.Request(BASE.format(p), headers={"User-Agent": "Mozilla/5.0 (X11; Linux) lectio"})
        h = urllib.request.urlopen(req, timeout=60).read().decode("utf-8", "replace")
        n = 0
        for tr in re.split(r"<tr\b", h):
            c = [x for x in re.findall(r"<td\b[^>]*>(.*?)</td>", tr, re.S)]
            if len(c) < 5: continue
            # the "Day" column sits at c[2] (Advent's 7-col table) or c[1] (the
            # Christmas 6-col table) — find it by which cell yields a slug, then the
            # readings follow it (first, psalm, ..., gospel last).
            di = next((i for i, cell in enumerate(c) if slug_of(cell)), None)
            if di is None or di + 1 >= len(c): continue
            slug = slug_of(c[di])
            cr[slug] = {"first": clean_cite(c[di + 1]),
                        "psalm": clean_cite(c[di + 2]) if di + 2 < len(c) - 1 else "",
                        "gospel": clean_cite(c[-1])}
            n += 1
        sys.stderr.write(f"{p}: {n} seasonal ferials mapped\n")

    text = open(INI).read()
    head = text[:text.index("\n[")]
    blocks, order = {}, []
    for b in re.split(r"\n(?=\[)", text[text.index("\n[") + 1:]):
        k = re.match(r"\[(.+?)\]", b)
        key = k.group(1) if k else b
        blocks[key] = b.rstrip("\n"); order.append(key)

    changed = firstdiff = added = 0
    for slug, p in cr.items():
        if not (p["first"] and p["gospel"]):
            continue
        for cyc in ("I", "II"):                      # seasonal ferials are single-cycle
            key = f"{slug}-{cyc}"
            old = dict(re.findall(r"^(\w+)\s*=\s*(.+)$", blocks.get(key, ""), re.M))
            if cyc == "I" and old.get("first") and firstbook(old["first"]) != firstbook(p["first"]):
                firstdiff += 1
                sys.stderr.write(f"  ~ {slug}: first {old['first']!r} -> {p['first']!r}\n")
            block = f"[{key}]\n" + "\n".join(f"{part} = {p[part]}" for part in ("first", "psalm", "gospel") if p[part])
            if key not in blocks:
                order.append(key); added += 1
            else:
                changed += 1
            blocks[key] = block
    open(INI, "w").write(head.rstrip("\n") + "\n\n" + "\n\n".join(blocks[k] for k in order) + "\n")
    sys.stderr.write(f"\nseasonal ferials: {changed} replaced, {added} new, {firstdiff} first-book changes\n")

if __name__ == "__main__":
    main()