aboutsummaryrefslogtreecommitdiff
path: root/scripts/genlect-of-season-ferials-cr.py
blob: 13c56a310d529bc921732d31b1abe00e3f604e4d (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
#!/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)
    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) < 6: continue
            slug = slug_of(c[2])
            if not slug: continue
            cr[slug] = {"first": clean_cite(c[3]), "psalm": clean_cite(c[4]), "gospel": clean_cite(c[-1])}
            n += 1
        sys.stderr.write(f"{p}: {n} week-based seasonal ferials\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()