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
|
package caldata
import (
"sort"
"strings"
"time"
"github.com/lukaszkasprzak/lectio/internal/calendar"
)
var partOrder = map[string]int{"first": 0, "psalm": 1, "second": 2, "acclamation": 3, "gospel": 4}
// ofFerial returns the day's Ordinary-Form ferial (weekday) readings, or nil.
func ofFerial(sel calendar.Selection, date time.Time, day calendar.LiturgicalDay) []calendar.Reading {
if day.WeekdayCycle == "" || date.Weekday() == time.Sunday {
return nil
}
if temp := calendar.Compute(date, sel, nil); temp.Observed.Layer == "temporal" {
return TemporalReadings("new", temp.Observed.Slug+"-"+day.WeekdayCycle)
}
return nil
}
// Readings resolves a day's Mass readings: the observed celebration's inline
// propers, else the temporal lectionary for its slug, else -- for an EF
// weekday with no proper Mass -- the preceding Sunday's Mass (the 1962 rule
// that green-season ferias repeat the Sunday). Shared by the CLI (cli.dayReadings
// delegates here) and calfeed.Build (injected as its readings func).
func Readings(sel calendar.Selection, layers []calendar.Layer, date time.Time, day calendar.LiturgicalDay) []calendar.Reading {
var rs []calendar.Reading
for _, m := range day.Observed.Masses {
rs = append(rs, m.Readings...)
}
if len(rs) > 0 {
// OF: a memorial with only a PROPER gospel (or only a proper first) takes
// the missing reading from the day's ferial -- e.g. the Guardian Angels are
// the ferial first reading + their proper gospel (Matt 18), and St Barnabas
// is his proper first (Acts 11) + the ferial gospel. A fully-proper feast
// (first AND gospel present) is left untouched.
have := map[string]bool{}
for _, r := range rs {
have[r.Part] = true
}
if sel.Form != "old" && !(have["first"] && have["gospel"]) {
for _, fr := range ofFerial(sel, date, day) {
if p := fr.Part; !have[p] && (p == "first" || p == "psalm" || p == "gospel") {
rs = append(rs, fr)
have[p] = true
}
}
sort.SliceStable(rs, func(i, j int) bool { return partOrder[rs[i].Part] < partOrder[rs[j].Part] })
}
return rs
}
// temporal table: EF by bare slug; OF by slug+cycle. Sundays & solemnities
// use the 3-year Sunday cycle (A/B/C); ferial weekdays use the 2-year
// weekday cycle (I/II). Sunday-slugs and weekday-slugs are disjoint, so
// trying both cycle suffixes is unambiguous.
if sel.Form == "old" {
if r := efBVMSaturdayReadings(day); len(r) > 0 {
return r
}
if r := efNativityOctaveReadings(day); len(r) > 0 {
return r
}
if r := TemporalReadings("old", day.Observed.Slug); len(r) > 0 {
return r
}
} else {
for _, cyc := range []string{day.SundayCycle, day.WeekdayCycle} {
if cyc == "" {
continue
}
if r := TemporalReadings("new", day.Observed.Slug+"-"+cyc); len(r) > 0 {
return r
}
}
}
// OF: a sanctoral memorial / optional memorial with no proper readings of its
// own reads the FERIAL (weekday) readings of the day -- the ordinary OF rule.
if sel.Form != "old" && day.Observed.Layer != "temporal" {
if r := ofFerial(sel, date, day); len(r) > 0 {
return r
}
}
// EF: a green-season weekday with no proper Mass repeats the preceding
// Sunday's Mass. Use the Sunday's TEMPORAL slug (not its observed) so the
// repeat still resolves when that Sunday was a feast (e.g. the Purification).
if sel.Form == "old" && day.Observed.Layer == "temporal" && date.Weekday() != time.Sunday {
sun := date.AddDate(0, 0, -int(date.Weekday())) // preceding Sunday (Sunday = 0)
return TemporalReadings(sel.Form, calendar.TemporalSlug(sun, sel))
}
return nil
}
// efBVMSaturdayReadings returns the Mass of Our Lady on Saturday, when that is
// the day's office.
//
// RG 78 keeps the office on a Saturday whose feria is IV class (see
// temporal_ef.go, which gives the day its white colour). RG 309(a) then says
// which Mass is said: "Missae de sancta Maria in sabbato ... iuxta temporum
// diversitatem" -- the Missal prints five formularies and the season selects
// among them. Without this the day was kept as Our Lady's but read the feria's
// Mass, so a Saturday in Septuagesima announced Our Lady and then read the
// parable of the labourers.
//
// Citations are colitur's, which verified each against both photographic scans
// of the 1962 Missal; see its lib/rites/rite_ef/lectionary_ef.ml,
// bvm_saturday_citations, for the per-formulary scan references.
//
// The Advent formulary is included and is STRUCTURALLY UNREACHABLE as the
// Saturday office: Advent has no IV-class ferias, so the office never falls
// there. It is kept because RG 309's subject is the Masses themselves, not
// only the days they land on -- the Mass exists -- and because a missing
// branch would silently fall through if the rank rules ever changed.
func efBVMSaturdayReadings(day calendar.LiturgicalDay) []calendar.Reading {
// The office, not merely the day: a Saturday IV-class feria that nothing
// outranked. A sanctoral feast that won the day keeps its own readings, so
// the observed slug must still be the temporal one -- which the office
// deliberately reuses rather than replacing (see temporal_ef.go).
if day.Weekday != time.Saturday || day.Observed.Rank != calendar.RankClass4 {
return nil
}
if !strings.HasPrefix(day.Observed.Slug, "ef-") || !strings.HasSuffix(day.Observed.Slug, "-saturday") {
return nil
}
first, gospel := "", ""
switch day.Season {
case calendar.Advent:
first, gospel = "Isa 7:10-15", "Luke 1:26-38"
case calendar.Christmas:
first, gospel = "Titus 3:4-7", "Luke 2:15-20"
case calendar.TimeAfterEpiphany:
// The Christmastide formulary runs on until the Purification (2
// February), not merely to the end of Christmas Time.
if m := day.Date.Month(); m == time.January || (m == time.February && day.Date.Day() <= 2) {
first, gospel = "Titus 3:4-7", "Luke 2:15-20"
} else {
first, gospel = "Ecclus 24:14-16", "Luke 11:27-28"
}
case calendar.Easter_:
first, gospel = "Ecclus 24:14-16", "John 19:25-27"
default: // Septuagesima, Lent, Passiontide, Time after Pentecost
first, gospel = "Ecclus 24:14-16", "Luke 11:27-28"
}
return []calendar.Reading{
{Part: "first", Citation: first},
{Part: "gospel", Citation: gospel},
}
}
// efNativityOctaveReadings returns the Mass of a WEEKDAY within the Octave of
// the Nativity -- 29, 30 and 31 December.
//
// The Missal gives those days a direct formulary, "Diebus infra octavam
// Nativitatis Domini, II classis": Titus 3:4-7 / Luke 2:15-20, and each date's
// own rubric points at it ("Die 29 decembris ... Missa Puer natus est nobis,
// ut supra", repeated verbatim on the 30th and 31st). They are named days in
// the calendarium too -- "De V die infra octavam Nativitatis Domini, II
// classis" and so on -- not ordinary ferias.
//
// lectio treated them as ordinary Christmas ferias, so they fell back to the
// preceding Sunday and served "Dominica infra octavam Nativitatis Domini"
// (Gal 4:1-7 / Luke 2:33-40). That is a DIFFERENT heading, for the Sunday
// specifically. colitur made the identical mistake once, measured it against
// real years and reverted it; its bootstrap_lectionary.ml records the
// rejection so it is not re-attempted, with the scan references for both.
//
// Only the WEEKDAYS. When 29-31 December falls on a Sunday the day really is
// "Dominica infra octavam" and keeps the Sunday's Mass, which is the
// distinction the wrong citation blurred -- so the Sunday is excluded here
// rather than merely happening not to match.
func efNativityOctaveReadings(day calendar.LiturgicalDay) []calendar.Reading {
if day.Date.Month() != time.December || day.Date.Day() < 29 {
return nil
}
if day.Weekday == time.Sunday {
return nil
}
// The day within the octave, not a feast that outranks it: the observed
// office must still be the temporal one. A sanctoral feast keeps its own
// readings, as it did before this.
if !strings.HasPrefix(day.Observed.Slug, "ef-christmas-") {
return nil
}
return []calendar.Reading{
{Part: "first", Citation: "Titus 3:4-7"},
{Part: "gospel", Citation: "Luke 2:15-20"},
}
}
|