aboutsummaryrefslogtreecommitdiff
path: root/internal/calendar/temporal_ef.go
blob: 0cd4b41ae37286f10cf00d886552da1be59760c9 (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
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
package calendar

import (
	"strconv"
	"strings"
	"time"
)

// Extraordinary Form (1962) season constants. Advent/Christmas/Lent/Easter are
// shared with the OF; these are EF-specific.
const (
	Septuagesima       Season = "septuagesima"
	Passiontide        Season = "passiontide"
	TimeAfterEpiphany  Season = "time-after-epiphany"
	TimeAfterPentecost Season = "time-after-pentecost"
)

// efChristTheKing is the last Sunday of October (the 1962 date — unlike the OF's
// last Sunday before Advent).
func efChristTheKing(year int) time.Time {
	oct31 := time.Date(year, 10, 31, 0, 0, 0, 0, time.UTC)
	return oct31.AddDate(0, 0, -int(oct31.Weekday())) // Sunday on/before Oct 31
}

// efColour is the default (Sunday/ferial) colour of an EF season.
func efColour(s Season) Colour {
	switch s {
	case Advent, Septuagesima, Lent, Passiontide:
		return Violet
	case Christmas, Easter_:
		return White
	default: // TimeAfterEpiphany, TimeAfterPentecost
		return Green
	}
}

// efSeason returns the 1962 season for date, by chronological date boundaries.
// efSlugSeason is the season used to NAME slugs and NUMBER weeks. It is not
// always the liturgical season: 6-13 January is liturgically Christmas Time
// (RG 72, see efSeason) while its days are still named and numbered from
// Epiphany, which is what the lectionary keys and every downstream consumer
// expect. Splitting the two is the whole point -- before this, one function
// served both and the wrong boundary in one was the wrong boundary in both.
func efSlugSeason(date time.Time, y int, easter time.Time) Season {
	adventThis := adventStart(y)
	christmasThis := time.Date(y, 12, 25, 0, 0, 0, 0, time.UTC)
	jan6 := time.Date(y, 1, 6, 0, 0, 0, 0, time.UTC)
	septStart := easter.AddDate(0, 0, -63) // Septuagesima Sunday
	ashWed := easter.AddDate(0, 0, -46)
	passionSun := easter.AddDate(0, 0, -14)
	paschalEnd := easter.AddDate(0, 0, 55) // Saturday after Pentecost; Trinity = +56

	switch {
	case !date.Before(adventThis) && date.Before(christmasThis):
		return Advent
	case !date.Before(christmasThis): // Dec 25..31
		return Christmas
	case date.Before(jan6): // Jan 1..5
		return Christmas
	case date.Before(septStart): // Epiphany .. before Septuagesima
		return TimeAfterEpiphany
	case date.Before(ashWed):
		return Septuagesima
	case date.Before(passionSun):
		return Lent
	case date.Before(easter):
		return Passiontide
	case !date.After(paschalEnd):
		return Easter_
	default:
		return TimeAfterPentecost
	}
}

func efCel(season Season, slug string, col Colour, rank Rank, week int) temporalDay {
	return temporalDay{
		Season: season, Colour: col, Week: week, Rank: rank,
		Sunday: false,
		Cel:    Celebration{Slug: slug, Rank: rank, Colour: col, Layer: "temporal"},
	}
}

// efSunday is efCel plus the Sunday flag, for the temporal candidates that
// are actual Sundays -- precedenceEF's tie-break needs to tell a Sunday (RG
// 91 entry 15, wins its tie) apart from an equal-class privileged FERIA
// (entry 18, the Ember/late-Advent days, which yields).
func efSunday(season Season, slug string, col Colour, rank Rank, week int) temporalDay {
	day := efCel(season, slug, col, rank, week)
	day.Sunday = true
	return day
}

// temporalEF computes the 1962 temporal identity of date. Season is exact
// (oracle-validated); ranks/weeks are best-effort for display.
// efSeason is the LITURGICAL season, which is what the colour follows.
//
// It differs from efSlugSeason on exactly one window. RG 72: "Tempus
// natalicium decurrit a I Vesperis Nativitatis Domini usque ad diem 13
// ianuarii INCLUSIVE" -- Christmas Time runs to 13 January, so 6-13 January is
// the Epiphany SECTION OF Christmas Time, not the start of Time after
// Epiphany. RG 119(a) backs the same boundary from the colour side, white
// "usque ad expletum tempus Epiphaniae". Those eight days were coming out
// green because this distinction did not exist.
//
// The slug season is deliberately left alone there: the days keep their
// Epiphany names and week numbers, which is what the lectionary keys and the
// generated downstream tables are built on. colitur, which models the two
// axes separately from the start, does the same -- season christmastide,
// slug ef-time-after-epiphany-sunday-1.
func efSeason(date time.Time, y int, easter time.Time) Season {
	if s := efSlugSeason(date, y, easter); s == TimeAfterEpiphany &&
		date.Month() == time.January && date.Day() <= 13 {
		return Christmas
	}
	return efSlugSeason(date, y, easter)
}

func temporalEF(date time.Time) temporalDay {
	date = date.UTC().Truncate(24 * time.Hour)
	y := date.Year()
	easter := Easter(y)
	slugSeason := efSlugSeason(date, y, easter)
	season := efSeason(date, y, easter)
	col := efColour(season)
	// The Octave of Pentecost (Whit Monday through the Ember Saturday, easter+50
	// to +55) is red, not the white of the rest of Paschaltide.
	if !date.Before(easter.AddDate(0, 0, 50)) && !date.After(easter.AddDate(0, 0, 55)) {
		col = Red
	}
	// Holy Thursday (the Mass of Chrism and the Mass in Cena Domini) is white,
	// a whole-Mass exception to Passiontide's violet -- RG 128(b)'s own named
	// exception list, and RG 122 stating the same fact affirmatively in the
	// White section.
	if sameDay(date, easter.AddDate(0, 0, -3)) {
		col = White
	}
	// Good Friday is BLACK. RG 128(b)'s exception list carries it in the same
	// sentence as Holy Thursday's -- "...Actione liturgica feria VI in Passione
	// et Morte Domini usque ad Communionem exclusive..." excepts the day from
	// Passiontide's violet -- and RG 132 assigns black to it. Adopted
	// 2026-08-18 from the sibling project colitur, which closed the same gap
	// against the Missal; this comment previously called it "a separate,
	// unmodelled gap" and it no longer is.
	//
	// The rubric is per-ACTION ("usque ad Communionem exclusive", violet
	// returns for the Communion rite) and this model carries one colour per
	// day, so black is the day's principal colour -- the same acknowledged
	// limit the Palm Sunday blessing already has. missalemeum's own colour set
	// for the day is "bv", black first, so even the upstream this file is
	// generated against agrees on the ordering.
	if sameDay(date, easter.AddDate(0, 0, -2)) {
		col = Black
	}
	sun := date.Weekday() == time.Sunday

	// Major feasts of the Lord (I class): nice titles + colour.
	switch {
	case date.Month() == time.December && date.Day() == 25:
		return efCel(Christmas, "ef-nativity", White, RankClass1, 0)
	case date.Month() == time.January && date.Day() == 1:
		return efCel(Christmas, "ef-circumcision", White, RankClass1, 0)
	// RG 17(a): "festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit
	// a die 2 ad 5 ianuarii, secus die 2 ianuarii" -- the Most Holy Name of
	// Jesus is kept on the Sunday falling 2-5 January, and when no Sunday falls
	// in that window, on 2 January instead.
	//
	// The fallback is not decoration: the window is four days wide, so in
	// roughly three years in seven no Sunday lands in it and 2 January carries
	// the feast. Without the fallback lectio simply had no Holy Name office at
	// all in those years.
	//
	// Placed before the Epiphany case below and after the Circumcision above,
	// so 1 January keeps its own feast and 6 January keeps Epiphany.
	case date.Month() == time.January && date.Day() >= 2 && date.Day() <= 5 &&
		date.Weekday() == time.Sunday:
		return efCel(Christmas, "ef-holy-name-sunday", White, RankClass2, 0)
	case date.Month() == time.January && date.Day() == 2 && !sundayFallsJan2to5(y):
		return efCel(Christmas, "ef-holy-name", White, RankClass2, 0)
	case date.Month() == time.January && date.Day() == 6:
		// Christmas, not TimeAfterEpiphany: RG 72 puts Epiphany INSIDE Christmas
		// Time, which runs to 13 January. The slug is unchanged.
		return efCel(Christmas, "ef-epiphany", White, RankClass1, 0)
	case sameDay(date, easter.AddDate(0, 0, -46)):
		return efCel(Lent, "ef-ash-wednesday", Violet, RankClass1, 0)
	case sameDay(date, easter.AddDate(0, 0, -14)):
		return efCel(Passiontide, "ef-passion-sunday", Violet, RankClass1, 1)
	case sameDay(date, easter.AddDate(0, 0, -7)):
		return efCel(Passiontide, "ef-palm-sunday", Violet, RankClass1, 2)
	case sameDay(date, easter):
		return efCel(Easter_, "ef-easter-sunday", White, RankClass1, 1)
	case sameDay(date, easter.AddDate(0, 0, 7)):
		return efCel(Easter_, "ef-low-sunday", White, RankClass1, 2)
	case sameDay(date, easter.AddDate(0, 0, 39)):
		return efCel(Easter_, "ef-ascension", White, RankClass1, 0)
	case sameDay(date, easter.AddDate(0, 0, 49)):
		return efCel(Easter_, "ef-pentecost", Red, RankClass1, 0)
	case sameDay(date, easter.AddDate(0, 0, 56)):
		return efCel(TimeAfterPentecost, "ef-trinity", White, RankClass1, 1)
	case sameDay(date, easter.AddDate(0, 0, 60)):
		return efCel(TimeAfterPentecost, "ef-corpus-christi", White, RankClass1, 0)
	case sameDay(date, easter.AddDate(0, 0, 68)):
		return efCel(TimeAfterPentecost, "ef-sacred-heart", White, RankClass1, 0)
	case sameDay(date, efChristTheKing(y)):
		return efCel(TimeAfterPentecost, "ef-christ-the-king", White, RankClass1, 0)
	case sameDay(date, easter.AddDate(0, 0, 38)): // Wednesday: Vigil of the Ascension (II class)
		return efCel(Easter_, "ef-ascension-vigil", White, RankClass2, 0)
	case sameDay(date, easter.AddDate(0, 0, 48)): // Saturday: Vigil of Pentecost / Whitsun Eve (I class)
		return efCel(Easter_, "ef-pentecost-vigil", Red, RankClass1, 0)
	}

	// Sundays vs ferias of the current season.
	if sun {
		week := efWeek(date, slugSeason, y, easter)
		rank := RankClass2
		sundayColour := col
		if season == Advent || season == Lent {
			// RG 11-12 / RG 91 entry 6: every Sunday of Advent and every Sunday
			// of Lent is I class -- not only Advent I, the sole case previously
			// handled (Passiontide's own two Sundays, Passion and Palm, are
			// already I class via the named-feast switch above; Low Sunday
			// likewise).
			rank = RankClass1
			switch {
			case season == Advent && week == 3:
				sundayColour = Rose // Gaudete -- RG 131, that Sunday's Office/Mass only
			case season == Lent && week == 4:
				sundayColour = Rose // Laetare -- RG 131, that Sunday's Office/Mass only
			}
		}
		// Resumed Sundays after Pentecost (1960 Rubrics): when Easter is early
		// there are more than 24 Sundays after Pentecost. The LAST Sunday before
		// Advent always keeps the 24th (Last) Mass; the surplus Sundays between the
		// 23rd and the last resume the Sundays after Epiphany that Septuagesima cut
		// short -- the highest-numbered ones, so the 6th sits just before the Last.
		if season == TimeAfterPentecost {
			lastSun := adventStart(y).AddDate(0, 0, -7)
			if sameDay(date, lastSun) {
				return efSunday(TimeAfterPentecost, "ef-time-after-pentecost-sunday-24", col, rank, 24)
			}
			if week > 23 {
				p := daysBetween(easter.AddDate(0, 0, 49), lastSun) / 7 // total Sundays after Pentecost
				e := week - p + 7                                       // resumed Sunday after Epiphany
				// Season stays time-after-pentecost (calendrical); the Epiphany
				// slug only routes the readings to the resumed Mass.
				return efSunday(TimeAfterPentecost, "ef-time-after-epiphany-sunday-"+strconv.Itoa(e), Green, rank, e)
			}
		}
		// Season reported liturgically, slug named from the slug season -- the
		// same split the resumed-Sunday branch just above already makes for
		// its own reason ("Season stays time-after-pentecost; the Epiphany
		// slug only routes the readings").
		return efSunday(season, "ef-"+string(slugSeason)+"-sunday-"+strconv.Itoa(week), sundayColour, rank, week)
	}
	// The days between Ash Wednesday and the 1st Sunday of Lent have their own
	// proper Masses (not part of a numbered Lenten week).
	if season == Lent && date.Before(easter.AddDate(0, 0, -42)) {
		return efCel(Lent, "ef-lent-after-ashes-"+weekdayLower(date), Violet, RankClass3, 0)
	}
	week := efWeek(date, slugSeason, y, easter)
	rank := RankClass4 // per-annum feria
	if season == Advent || season == Lent || season == Passiontide {
		// III class ferias (1960 rubrics): Advent to Dec 16, and Lent/Passiontide.
		// These outrank III class feasts (the saint is only commemorated). The
		// Septuagesima season's ferias are IV class per annum, so a III class
		// saint (e.g. the Conversion of St Paul, Jan 25) displaces them.
		rank = RankClass3
	}
	if season == Advent && date.Month() == time.December && date.Day() >= 17 && date.Day() <= 23 {
		// RG 91 entry 18: the late-Advent ferias (17-23 Dec) are II class
		// privileged ferias, not the ordinary III class of the rest of Advent.
		rank = RankClass2
	}
	if season == Christmas && date.Month() == time.December && date.Day() >= 26 {
		// RG 67/68: "Dies infra octavam sunt II classis" -- days within the
		// Octave of the Nativity (26-31 Dec) are II class; the octave day
		// itself (1 Jan) is I class, already handled above as a named feast.
		rank = RankClass2
	}
	if efPrivilegedFeria(date, easter) {
		// The days of Holy Week and the privileged octaves of Easter and
		// Pentecost are I class; no saint's feast is admitted (the feast is
		// transferred or, in the octaves, omitted). Ranking them class-1 makes
		// the temporal office win, matching the 1962 occurrence rules.
		rank = RankClass1
	}
	// The Ember Days of September, Advent and Lent (Wednesday, Friday, Saturday
	// after the anchor Sunday) are II class privileged ferias in violet; they
	// displace a III class saint (only commemorated) -- RG 91 entry 18. The
	// Whitsun Ember days fall inside the I class Pentecost octave handled above.
	if es, ok := efEmberSlug(date, y, easter); ok {
		return efCel(season, es, Violet, RankClass2, week)
	}
	// Unique ferial slug (season-week-weekday) so proper ferias (Lent, Advent,
	// Holy Week, Ember days) can key their own readings; green-season ferias
	// simply have no lectionary entry and fall back to the Sunday.
	// RG 87: "Litaniae minores seu Rogationes, per se, assignantur feriis II,
	// III et IV ante festum Ascensionis Domini" -- the Minor Litanies are
	// assigned to the Monday, Tuesday and Wednesday before the Ascension.
	// Easter+36 and +37 are that Monday and Tuesday; their colour is violet,
	// RG 128(d) giving violet "for the procession and Mass of the Greater and
	// Lesser Litanies", a penitential note inside white Paschaltide.
	//
	// The WEDNESDAY is deliberately absent. Easter+38 is by construction the
	// Vigil of the Ascension, which is II class and wins the day outright, so
	// there is no Rogation OFFICE to build there -- what the rubric asks for on
	// that day is a commemoration, and that needs a channel this function does
	// not have (it returns one office per day). colitur reaches it through a
	// movable-date entry in its own sanctoral overlay; here it stays unbuilt
	// rather than half-built, which is why this comment says so.
	//
	// The rank is left as the feria's: these are IV-class days per annum and
	// any III-class saint displaces them, which is what makes the office rare
	// -- in 2026 all three are impeded and none appears at all.
	if rog := daysBetween(easter, date); rog == 36 || rog == 37 {
		slug := "ef-rogation-monday"
		if rog == 37 {
			slug = "ef-rogation-tuesday"
		}
		return efCel(season, slug, Violet, rank, week)
	}
	// RG 78 (Caput IX, "De sancta Maria in sabbato"): "In sabbatis, in quibus
	// occurrit Officium de feria IV classis, fit de sancta Maria in sabbato."
	// On a Saturday whose office would otherwise be a IV-class feria, the
	// office is the votive Office of Our Lady, and it is white -- RG 431(e)
	// classes that Mass as "Missa votiva IV classis ... de B. Maria Virg.",
	// RG 121(a) gives a votive Mass the colour of the feast-type it answers
	// to, and RG 120(b) makes feasts of the BVM white.
	//
	// The protasis is "a IV-class feria", which by this point in the function
	// is exactly what rank still being RankClass4 means: every branch above
	// has already promoted Advent, Lent, Passiontide, the Ember days, the
	// privileged ferias and the Christmas octave out of it. Nothing further
	// needs testing.
	//
	// The SLUG IS DELIBERATELY UNCHANGED. colitur, which has built this office
	// since its v0.3.0, does the same and for a reason worth repeating: a
	// bespoke slug would recur many times a year and break the "one slug per
	// liturgical year" invariant, and the day's readings are selected by the
	// Mass formulary rather than by the slug. Only the colour moves here.
	//
	// This is the temporal office only. If a sanctoral feast outranks the
	// IV-class Saturday it wins as before and brings its own colour, exactly
	// as it did when this day was a plain green feria.
	if rank == RankClass4 && date.Weekday() == time.Saturday {
		col = White
	}
	return efCel(season, "ef-"+string(slugSeason)+"-"+strconv.Itoa(week)+"-"+weekdayLower(date), col, rank, week)
}

// sundayFallsJan2to5 reports whether any Sunday falls in RG 17(a)'s own window,
// 2 to 5 January inclusive. When one does the Holy Name is kept on it; when
// none does the feast falls back to 2 January, which is what "secus die 2
// ianuarii" provides for.
func sundayFallsJan2to5(y int) bool {
	for day := 2; day <= 5; day++ {
		if time.Date(y, time.January, day, 0, 0, 0, 0, time.UTC).Weekday() == time.Sunday {
			return true
		}
	}
	return false
}

// firstSundayAfterEpiphany returns the first Sunday strictly after 6 January
// of year y -- the anchor the weeks after Epiphany are numbered from. When
// Epiphany itself falls on a Sunday the next one is meant, which is why the
// loop starts on 7 January rather than on the 6th.
func firstSundayAfterEpiphany(y int) time.Time {
	d := time.Date(y, time.January, 7, 0, 0, 0, 0, time.UTC)
	for d.Weekday() != time.Sunday {
		d = d.AddDate(0, 0, 1)
	}
	return d
}

// thirdSundayOfSeptember returns the third Sunday in September of year y.
func thirdSundayOfSeptember(y int) time.Time {
	d := time.Date(y, time.September, 1, 0, 0, 0, 0, time.UTC)
	for d.Weekday() != time.Sunday {
		d = d.AddDate(0, 0, 1)
	}
	return d.AddDate(0, 0, 14)
}

// efEmberSlug returns the Ember-day slug for date, if it is an Ember Wednesday,
// Friday or Saturday of September, Advent or Lent (the day after the anchor
// Sunday + 3/5/6: the third Sunday of September, Advent III, or Lent I --
// MR1962, "De anno et eius partibus", Quatuor Tempora). Returns ("", false)
// otherwise. The Whitsun Ember days are not listed here: they fall inside the
// I class Pentecost octave, handled separately by efPrivilegedFeria.
func efEmberSlug(date time.Time, y int, easter time.Time) (string, bool) {
	for _, e := range []struct {
		anchor time.Time
		season string
	}{
		{thirdSundayOfSeptember(y), "september"},
		{adventStart(y).AddDate(0, 0, 14), "advent"}, // 3rd Sunday of Advent
		{easter.AddDate(0, 0, -42), "lent"},          // 1st Sunday of Lent
	} {
		switch {
		case sameDay(date, e.anchor.AddDate(0, 0, 3)):
			return "ef-" + e.season + "-ember-wed", true
		case sameDay(date, e.anchor.AddDate(0, 0, 5)):
			return "ef-" + e.season + "-ember-fri", true
		case sameDay(date, e.anchor.AddDate(0, 0, 6)):
			return "ef-" + e.season + "-ember-sat", true
		}
	}
	return "", false
}

func weekdayLower(d time.Time) string {
	return strings.ToLower(d.Weekday().String())
}

// efPrivilegedFeria reports whether date is an I class temporal weekday that
// impedes saints' feasts: the ferias of Holy Week (the six days before Easter)
// and the weekdays within the privileged octaves of Easter (Easter Mon–Sat) and
// Pentecost (Whit Mon–Sat). Easter/Pentecost Sundays and Low Sunday are already
// handled as named I class feasts above; this covers only their octave weekdays.
func efPrivilegedFeria(date, easter time.Time) bool {
	days := int(date.Sub(easter) / (24 * time.Hour))
	switch {
	case days >= -6 && days <= -1: // Monday..Saturday of Holy Week (incl. Triduum)
		return true
	case days >= 1 && days <= 6: // Easter octave weekdays
		return true
	case days >= 50 && days <= 55: // Pentecost octave weekdays (Pentecost = easter+49)
		return true
	}
	return false
}

// efWeek is a best-effort week-within-season number for display (not
// oracle-checked). Time after Pentecost counts Sundays from Pentecost.
func efWeek(date time.Time, season Season, y int, easter time.Time) int {
	switch season {
	case Advent:
		return daysBetween(adventStart(y), date)/7 + 1
	case TimeAfterPentecost:
		return daysBetween(easter.AddDate(0, 0, 49), date) / 7 // Sundays after Pentecost
	case Lent:
		return daysBetween(easter.AddDate(0, 0, -42), date)/7 + 1 // from 1st Sunday of Lent
	case TimeAfterEpiphany:
		// Weeks after Epiphany are numbered from the FIRST SUNDAY after
		// Epiphany, not from Epiphany itself. Counting from 6 January put every
		// week one too high: 14 January 2026 came out as week 2 when the first
		// Sunday after Epiphany was 11 January and the week running from it is
		// the first.
		//
		// The Sunday is the anchor because it is the Sunday that carries the
		// Mass the week is named for -- "Dominica I post Epiphaniam" and then
		// its ferias. Epiphany itself is a feast inside Christmas Time (RG 72),
		// not the head of a numbered week.
		return daysBetween(firstSundayAfterEpiphany(y), date)/7 + 1
	case Passiontide:
		// Passiontide is TWO weeks and they are not interchangeable: Passion
		// week (from Passion Sunday, Easter-14) and Holy Week (from Palm
		// Sunday, Easter-7) have entirely different Masses. Numbering them
		// both 0 -- which is what falling through to the default did -- gave
		// every day of Holy Week the slug of its Passion-week namesake, so the
		// two weeks shared one set of readings and Holy Week could not have
		// its own.
		return daysBetween(easter.AddDate(0, 0, -14), date)/7 + 1
	case Septuagesima:
		return daysBetween(easter.AddDate(0, 0, -63), date)/7 + 1
	case Easter_:
		return daysBetween(easter, date)/7 + 1
	default:
		return 0
	}
}