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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
|
//go:build ignore
// genlect-of generates the OF (Ordinary Form) temporal lectionary from
// niedziela.pl (via internal/readings): Sundays & solemnities keyed by
// <Observed.Slug>-<SundayCycle> (A/B/C), ferial weekdays by <ferial-slug>-
// <WeekdayCycle> (I/II). Only TRUE ferials are harvested for the weekday table;
// memorial days are skipped (their readings come from the ferial fallback at
// runtime). One-time; requires network. Run from the repo root:
//
// go run scripts/genlect-of.go
//
// Writes internal/caldata/of-lectionary.ini.
package main
import (
"fmt"
"os"
"regexp"
"sort"
"strings"
"time"
"github.com/lukaszkasprzak/lectio/internal/bible"
"github.com/lukaszkasprzak/lectio/internal/caldata"
"github.com/lukaszkasprzak/lectio/internal/calendar"
"github.com/lukaszkasprzak/lectio/internal/config"
"github.com/lukaszkasprzak/lectio/internal/readings"
)
// bookCommaRe strips a stray comma between the book name and the first
// chapter ("4 Kings, 5:1-15" -> "4 Kings 5:1-15"), a source 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.
// Copied verbatim from scripts/genlect.go.
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 source 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.
// Copied verbatim from scripts/genlect.go.
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
}
// numBookSpaceRe restores the space niedziela sometimes drops in a numbered
// book sigil ("1J 3,11" -> "1 J 3,11"; "2Kor" -> "2 Kor") so ToEnglishRef's
// Polish table (keyed "1 J", "2 Kor", …) matches. Applied to the raw Polish
// citation BEFORE ToEnglishRef.
var numBookSpaceRe = regexp.MustCompile(`^(\d)([^\d\s])`)
// sourceGlitches are exact niedziela sigla-casing glitches that don't match the
// ToEnglishRef Polish table (which is Title-case). E.g. "PnP" for Song of Songs.
var sourceGlitches = map[string]string{"PnP": "Pnp"}
// fixSourceCite normalises a raw niedziela citation before ToEnglishRef.
func fixSourceCite(s string) string {
s = numBookSpaceRe.ReplaceAllString(strings.TrimSpace(s), "$1 $2")
for bad, good := range sourceGlitches {
if strings.HasPrefix(s, bad+" ") {
s = good + s[len(bad):]
}
}
return s
}
// partIDToPart maps a niedziela.pl (liturgy.Section) PartID to our lectionary
// field name. Anything absent (e.g. "aklamacja") is not a reading and is
// ignored.
var partIDToPart = map[string]string{
"pierwsze_czytanie": "first",
"psalm": "psalm",
"drugie_czytanie": "second",
"ewangelia": "gospel",
}
func main() {
sel := calendar.DefaultSelection()
sel.Form = "new"
layers := []calendar.Layer{caldata.Universal()}
cfg := config.Default()
cfg.Lectionary = "new"
lect := map[string]map[string]string{} // key -> part -> citation
seen := map[string]bool{}
missing := map[string]string{} // key -> last failure reason; cleared on success
// niedziela only publishes ~6 weeks ahead, so future years are unavailable;
// the cycles repeat, so a liturgical position's readings are identical
// whichever civil year they fall in. Harvest 2018-2025 -- TWO+ years per
// cycle (Sunday A:2020,2023 B:2021,2024 C:2022,2025; weekday I:2021,2023,2025
// II:2020,2022,2024) so a position displaced by a saint/feast in one year is
// still captured in another. A key is stored from the FIRST year it resolves;
// a year where it fails does not block a later retry (seen is set on success).
start := time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC)
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
full := calendar.Compute(d, sel, layers)
sunOrSol := d.Weekday() == time.Sunday ||
full.Observed.Rank == calendar.RankSolemnity ||
full.Observed.Rank == calendar.RankFeast
var key string
if sunOrSol {
key = full.Observed.Slug + "-" + full.SundayCycle
} else if full.Observed.Rank == calendar.RankFerial && full.Observed.Layer == "temporal" {
// A true ferial (incl. optional-memorial days, where the ferial is the
// observed default): niedziela shows the ferial readings.
key = full.Observed.Slug + "-" + full.WeekdayCycle
} else {
// Obligatory memorial: SKIP. Most memorials read the ferial of the
// DAY, which varies year to year (the fixed feast date lands in a
// different Ordinary-Time week each year) -- so keying by the saint
// slug would freeze one year's ferial and be wrong in others. At
// runtime the resolver falls the memorial back to the day's ferial
// slug, which resolves to the correct week's reading. (Proper-reading
// memorials therefore show the ferial, a permitted OF option; their
// propers are a later refinement.)
continue
}
if seen[key] {
continue // already stored from an earlier year
}
dateStr := d.Format("2006-01-02")
secs, _, err := readings.Load(cfg, readings.Options{Date: dateStr, All: true})
if err != nil {
fmt.Fprintf(os.Stderr, "%s %-32s ERROR readings.Load: %v\n", dateStr, key, err)
missing[key] = fmt.Sprintf("%s (readings.Load error: %v)", dateStr, err)
continue
}
parts := map[string]string{}
for _, s := range secs {
part, ok := partIDToPart[s.PartID]
if !ok {
continue
}
system := ""
if part == "psalm" {
system = "drb"
}
eng, err := bible.ToEnglishRef(fixSourceCite(s.Citation), system)
if err != nil {
fmt.Fprintf(os.Stderr, "%s %-32s part=%-6s citation=%q: ToEnglishRef error: %v\n",
dateStr, key, part, s.Citation, err)
continue
}
parts[part] = cleanCite(eng)
}
if parts["first"] == "" || parts["gospel"] == "" {
missing[key] = fmt.Sprintf("%s (first=%q gospel=%q)", dateStr, parts["first"], parts["gospel"])
continue // require at least first+gospel to store the entry
}
lect[key] = parts
seen[key] = true // store from the first year it resolves; stop retrying
delete(missing, key) // a later year succeeded; not actually missing
fmt.Fprintf(os.Stderr, "%s %-32s first=%-22s psalm=%-22s second=%-22s gospel=%s\n",
dateStr, key, parts["first"], parts["psalm"], parts["second"], parts["gospel"])
}
keys := make([]string, 0, len(lect))
for k := range lect {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
b.WriteString("; OF (Ordinary Form) temporal lectionary, keyed by <computed-temporal-slug>-<cycle>:\n")
b.WriteString("; Sundays & solemnities use the Sunday cycle (A/B/C), ferial weekdays the weekday\n")
b.WriteString("; cycle (I/II). Citations English-canonical.\n")
b.WriteString("; Generated from niedziela.pl by scripts/genlect-of.go (harvest 2018-2025).\n")
for _, k := range keys {
v := lect[k]
fmt.Fprintf(&b, "\n[%s]\n", k)
for _, part := range []string{"first", "psalm", "second", "gospel"} {
if c := v[part]; c != "" {
fmt.Fprintf(&b, "%s = %s\n", part, c)
}
}
}
if err := os.WriteFile("internal/caldata/of-lectionary.ini", []byte(b.String()), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "\nwrote %d OF temporal entries\n", len(lect))
if len(missing) > 0 {
mk := make([]string, 0, len(missing))
for k := range missing {
mk = append(mk, k)
}
sort.Strings(mk)
fmt.Fprintf(os.Stderr, "%d keys NEVER resolved in any harvest year:\n", len(missing))
for _, k := range mk {
fmt.Fprintf(os.Stderr, " %-34s %s\n", k, missing[k])
}
}
}
|