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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
//go:build ignore
// genlect generates the EF temporal Sunday lectionary from missalemeum
// (Divinum Officium data), keyed by lectio's computed temporal-day slug.
// One-time; requires network. Run from the repo root:
//
// go run scripts/genlect.go
//
// Writes internal/caldata/tridentine-lectionary.ini.
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"regexp"
"sort"
"strings"
"time"
"github.com/lukaszkasprzak/lectio/internal/caldata"
"github.com/lukaszkasprzak/lectio/internal/calendar"
)
var citeRe = regexp.MustCompile(`\*([^*]+)\*`)
// bookCommaRe strips a stray comma between the book name and the first
// chapter ("4 Kings, 5:1-15" -> "4 Kings 5:1-15"), a missalemeum data glitch.
// The [^:] guard means it only fires before any chapter:verse, so legitimate
// multi-chapter commas ("Gen 1:1, 2:3") are left intact.
var bookCommaRe = regexp.MustCompile(`^([^:]*?),\s+(\d+:)`)
// chapDotRe rewrites a European-style "chapter. verse" separator to a colon
// ("John 20. 19-31" -> "John 20:19-31"), another missalemeum data glitch. It is
// applied only when the citation carries no colon at all, so disjoint verse
// groups in a normal citation ("Ps 62:2. 3-4") are never touched.
var chapDotRe = regexp.MustCompile(`(\d+)\.\s+(\d)`)
func cleanCite(s string) string {
s = bookCommaRe.ReplaceAllString(strings.TrimSpace(s), "$1 $2")
if !strings.Contains(s, ":") {
s = chapDotRe.ReplaceAllString(s, "$1:$2")
}
return s
}
func fetchCitations(date string) (epistle, gospel string) {
resp, err := http.Get("https://www.missalemeum.com/en/api/v5/proper/" + date)
if err != nil {
return
}
defer resp.Body.Close()
var data []struct {
Sections []struct {
ID string `json:"id"`
Body [][]string `json:"body"` // [[english, latin]]
} `json:"sections"`
}
if json.NewDecoder(resp.Body).Decode(&data) != nil || len(data) == 0 {
return
}
for _, s := range data[0].Sections {
if len(s.Body) == 0 || len(s.Body[0]) == 0 {
continue
}
m := citeRe.FindStringSubmatch(s.Body[0][0]) // english text carries *citation*
if m == nil {
continue
}
switch s.ID {
case "Lectio":
epistle = cleanCite(m[1])
case "Evangelium":
gospel = cleanCite(m[1])
}
}
return
}
func main() {
sel := calendar.DefaultSelection()
sel.Form = "old"
layers := []calendar.Layer{caldata.Tridentine()}
lect := map[string][2]string{} // slug -> {epistle, gospel}
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2028, 12, 31, 0, 0, 0, 0, time.UTC)
seen := map[string]bool{}
// Seasons whose weekdays may have PROPER Masses (else a ferial repeats the
// preceding Sunday and is resolved by the CLI fallback, so we skip it).
properFerial := map[calendar.Season]bool{
calendar.Advent: true, calendar.Lent: true,
calendar.Passiontide: true, calendar.Easter_: true,
}
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
day := calendar.Compute(d, sel, layers)
if day.Observed.Layer != "temporal" { // a feast won -> its own propers
continue
}
slug := day.Observed.Slug
isSunday := d.Weekday() == time.Sunday
// Ember and vigil days have a proper Mass even in the green seasons, so
// harvest them regardless of season (green ferias otherwise repeat the
// preceding Sunday and are skipped).
special := strings.Contains(slug, "ember") || strings.Contains(slug, "vigil")
if !isSunday && !properFerial[day.Season] && !special {
continue // green-season feria -> repeats the Sunday
}
if seen[slug] {
continue
}
ep, gos := fetchCitations(d.Format("2006-01-02"))
if ep == "" && gos == "" {
continue
}
seen[slug] = true
if !isSunday {
// store only if the reading differs from the preceding Sunday's Mass
sun := d.AddDate(0, 0, -int(d.Weekday()))
if r, ok := lect[calendar.Compute(sun, sel, layers).Observed.Slug]; ok && r[0] == ep && r[1] == gos {
continue // a repeat -> the CLI fallback handles it
}
}
lect[slug] = [2]string{ep, gos}
fmt.Fprintf(os.Stderr, "%s %-38s ep=%-22s go=%s\n", d.Format("2006-01-02"), slug, ep, gos)
}
slugs := make([]string, 0, len(lect))
for s := range lect {
slugs = append(slugs, s)
}
sort.Strings(slugs)
var b strings.Builder
b.WriteString("; EF (1962) temporal lectionary, keyed by lectio's computed temporal-day slug.\n")
b.WriteString("; Generated from missalemeum (Divinum Officium). Epistle (first) + Gospel citations.\n")
for _, s := range slugs {
v := lect[s]
fmt.Fprintf(&b, "\n[%s]\n", s)
if v[0] != "" {
fmt.Fprintf(&b, "first = %s\n", v[0])
}
if v[1] != "" {
fmt.Fprintf(&b, "gospel = %s\n", v[1])
}
}
if err := os.WriteFile("internal/caldata/tridentine-lectionary.ini", []byte(b.String()), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "wrote %d temporal entries\n", len(lect))
}
|