diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 08:49:57 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 08:49:57 +0200 |
| commit | 70b805018f0ae0d8424c3c7de8f1169476385a35 (patch) | |
| tree | 82de4bf52e588ab0c1f31ce0fc68d4b97b3d0de6 | |
| parent | d3b7f41c7a823bda6b7bafae3a156348c4ce687f (diff) | |
| download | lectio-70b805018f0ae0d8424c3c7de8f1169476385a35.tar.gz lectio-70b805018f0ae0d8424c3c7de8f1169476385a35.zip | |
feat: complete OF+EF calendar coverage (movable feasts, vigils, ember days, transfers)
Ordinary Form:
- Add the movable Marian memorials that have no fixed date: Mary, Mother of the
Church (Monday after Pentecost) and the Immaculate Heart (Saturday after the
Sacred Heart), computed in computeOF.
- General Norms 60: two coinciding obligatory memorials both become optional and
the ferial is observed -- except Mary, Mother of the Church, which keeps
precedence per its 2018 decree.
Extraordinary Form:
- Capture fixed-date vigils in the sanctoral harvest (Christmas, the Nativity of
St John the Baptist, Sts Peter & Paul, St Lawrence, the Assumption, All Saints,
the Immaculate Conception) -- previously filtered out.
- temporalEF: the movable vigils of the Ascension (I class eve) and Pentecost
(Whitsun Eve), and the September/Advent Ember days (II class violet ferias).
- transferIfImpededEF: the Annunciation transfers past Holy Week/the Easter
octave to the Monday after Low Sunday; All Souls moves to Nov 3 when Nov 2 is a
Sunday.
- genlect harvests proper Masses for the new Ember/vigil days.
Adds TestEFCoverage + TestOFMovableMemorials. Bumps to 0.39.0.
Verified per-day: OF 0 real errors vs calapi (2026-2028, 100% correct-observed);
EF 0 real errors vs missalemeum (2025-2027), residual = deep-tail vigil-occurrence
and St Joseph Passiontide-transfer edges (~1-2 days/yr, documented).
| -rw-r--r-- | internal/caldata/tridentine-calendar.ini | 40 | ||||
| -rw-r--r-- | internal/caldata/tridentine-lectionary.ini | 166 | ||||
| -rw-r--r-- | internal/calendar/calendar.go | 62 | ||||
| -rw-r--r-- | internal/calendar/coverage_test.go | 75 | ||||
| -rw-r--r-- | internal/calendar/precedence.go | 31 | ||||
| -rw-r--r-- | internal/calendar/temporal_ef.go | 44 | ||||
| -rw-r--r-- | internal/config/config.go | 2 | ||||
| -rw-r--r-- | scripts/gen-sanctoral-ef.go | 11 | ||||
| -rw-r--r-- | scripts/genlect.go | 6 |
9 files changed, 298 insertions, 139 deletions
diff --git a/internal/caldata/tridentine-calendar.ini b/internal/caldata/tridentine-calendar.ini index aaa9841..cce1a65 100644 --- a/internal/caldata/tridentine-calendar.ini +++ b/internal/caldata/tridentine-calendar.ini @@ -974,6 +974,14 @@ name.en = St. Paulinus of Nola reading.first = 2 Cor. 8:9-15 reading.gospel = Luke 12:32-34 +[vigil-of-the-nativity-of-st-john-the-baptist] +date = 06-23 +rank = class-2 +colour = violet +name.en = Vigil of the Nativity of St. John the Baptist +reading.first = Jer 1:4-10 +reading.gospel = Luke 1:5-17 + [nativity-of-st-john-the-baptist] date = 06-24 rank = class-1 @@ -998,6 +1006,14 @@ name.en = Sts. John & Paul reading.first = Eccli 44:10-15 reading.gospel = Luke 12:1-8 +[vigil-of-sts-peter-paul] +date = 06-28 +rank = class-2 +colour = violet +name.en = Vigil of Sts. Peter & Paul +reading.first = Acts 3:1-10 +reading.gospel = John 21:15-19 + [sts-peter-paul] date = 06-29 rank = class-1 @@ -1352,6 +1368,14 @@ rank = commemoration colour = red name.en = St. Romanus +[vigil-of-st-lawrence] +date = 08-09 +rank = class-3 +colour = red +name.en = Vigil of St. Lawrence +reading.first = Ecclus 51:1-8, 12 +reading.gospel = Matt 16:24-27 + [lawrence] date = 08-10 rank = class-2 @@ -1380,6 +1404,14 @@ rank = commemoration colour = red name.en = Sts. Hippolytus & Cassian +[vigil-of-the-assumption] +date = 08-14 +rank = class-2 +colour = white +name.en = Vigil of the Assumption +reading.first = Sir 24:23-31 +reading.gospel = Luke 11:27-28 + [assumption-of-the-blessed-virgin-mary] date = 08-15 rank = class-1 @@ -2289,6 +2321,14 @@ name.en = St. Thomas reading.first = Eph 2:19-22 reading.gospel = John 20:24-29 +[vigil-of-christmas] +date = 12-24 +rank = class-1 +colour = violet +name.en = Vigil of Christmas +reading.first = Rom 1:1-6 +reading.gospel = Matt 1:18-21 + [stephen] date = 12-26 rank = class-2 diff --git a/internal/caldata/tridentine-lectionary.ini b/internal/caldata/tridentine-lectionary.ini index 372be6b..cef3f2d 100644 --- a/internal/caldata/tridentine-lectionary.ini +++ b/internal/caldata/tridentine-lectionary.ini @@ -1,30 +1,6 @@ ; EF (1962) temporal lectionary, keyed by lectio's computed temporal-day slug. ; Generated from missalemeum (Divinum Officium). Epistle (first) + Gospel citations. -[ef-advent-1-friday] -first = Heb 13:7-17 -gospel = Matt 25:14-23 - -[ef-advent-1-monday] -first = Sir 51:13-17. -gospel = Matt 13:44-52. - -[ef-advent-1-saturday] -first = 2 Tim 4:1-8 -gospel = Matt 5:13-19 - -[ef-advent-1-tuesday] -first = Rom 10:10-18 -gospel = Mark 16:15-18 - -[ef-advent-1-wednesday] -first = 2 Tim 4:1-8 -gospel = Matt 5:13-19 - -[ef-advent-2-friday] -first = 2 Cor 10:17-18; 11:1-2 -gospel = Matt 13:44-52. - [ef-advent-2-monday] first = Rom 15:4-13 gospel = Matt 11:2-10 @@ -41,42 +17,18 @@ gospel = Matt 11:2-10 first = Rom 15:4-13 gospel = Matt 11:2-10 -[ef-advent-2-wednesday] -first = 1 Pet 5:1-4; 5:10-11. -gospel = Matt 16:13-19 - -[ef-advent-3-friday] +[ef-advent-ember-fri] first = Isa 11:1-5 gospel = Luke 1:39-47 -[ef-advent-3-monday] -first = 2 Cor. 1:3-7 -gospel = Matt 16:24-27. - -[ef-advent-3-saturday] +[ef-advent-ember-sat] first = Eph 2:19-22 gospel = John 20:24-29 -[ef-advent-3-wednesday] +[ef-advent-ember-wed] first = Isa 7:10-15 gospel = Luke 1:26-38 -[ef-advent-4-friday] -first = Rom 1:1-6 -gospel = Matt 1:18-21 - -[ef-advent-4-thursday] -first = Rom 1:1-6 -gospel = Matt 1:18-21 - -[ef-advent-4-tuesday] -first = Rom 1:1-6 -gospel = Matt 1:18-21 - -[ef-advent-4-wednesday] -first = Rom 1:1-6 -gospel = Matt 1:18-21 - [ef-advent-sunday-1] first = Rom 13:11-14 gospel = Luke 21:25-33 @@ -97,6 +49,10 @@ gospel = Luke 3:1-6 first = Acts 1:1-11 gospel = Mark 16:14-20 +[ef-ascension-vigil] +first = Eph. 4:7-13. +gospel = John 17:1-11. + [ef-ash-wednesday] first = Joel 2:12-19 gospel = Matt 6:16-21 @@ -133,93 +89,21 @@ gospel = Luke 24:36-47 first = Acts 3:13-15; 3:17-19 gospel = John 21:1-14 -[ef-easter-2-monday] -first = Isa 7:10-15 -gospel = Luke 1:26-38 - [ef-easter-2-saturday] -first = Wis 10:10-14 -gospel = Luke 14:26-33. - -[ef-easter-2-thursday] -first = 1 Pet 5:1-4; 5:10-11. -gospel = Matt 16:13-19 +first = Ecclus 24:14-16 +gospel = John 19:25-27 [ef-easter-3-saturday] first = Ecclus 24:14-16 gospel = John 19:25-27 -[ef-easter-4-friday] -first = 1 Pet 5:1-4; 5:10-11. -gospel = Matt 16:13-19 - -[ef-easter-4-monday] -first = 1 Pet 5:1-4; 5:10-11. -gospel = Matt 16:13-19 - -[ef-easter-4-saturday] -first = 2 Tim 4:1-8 -gospel = Matt 5:13-19 - -[ef-easter-4-thursday] -first = Ezek 1:10-14 -gospel = Luke 10:1-9 - -[ef-easter-4-wednesday] -first = Wis 5:1-5 -gospel = John 15:1-7 - -[ef-easter-5-monday] -first = 2 Tim. 2:8-10; 3:10-12. -gospel = Matt 10:34-42 - [ef-easter-5-saturday] -first = 1 Tim. 5:3-10. -gospel = Luke 7:11-16 - -[ef-easter-5-thursday] -first = 2 Cor. 4:5-14 -gospel = Matt 10:23-28 - -[ef-easter-5-tuesday] -first = 2 Cor 10:17-18; 11:1-2 -gospel = Matt 25:1-13. - -[ef-easter-5-wednesday] -first = Col. 3:14-15, 17, 23-24 -gospel = Matt 13:54-58 - -[ef-easter-6-friday] -first = Sir 44:16-27; 45:3-20 -gospel = Matt 25:14-23 +first = Ecclus 24:14-16 +gospel = John 19:25-27 [ef-easter-6-saturday] -first = Wis. 5:1-5 -gospel = John 14:1-13 - -[ef-easter-6-tuesday] -first = Wis 5:1-5 -gospel = John 15:1-7 - -[ef-easter-6-wednesday] -first = Eph. 4:7-13. -gospel = John 17:1-11. - -[ef-easter-7-friday] -first = Sir 31:8-11 -gospel = Luke 12:35-40 - -[ef-easter-7-monday] -first = Wis 7:7-14. -gospel = Matt 5:13-19 - -[ef-easter-7-saturday] -first = Acts 19:1-8. -gospel = John 14:15-21. - -[ef-easter-7-wednesday] -first = Sir 31:8-11 -gospel = Matt 18:1-5 +first = Ecclus 24:14-16 +gospel = John 19:25-27 [ef-easter-8-friday] first = Joel 2:23-24; 26-27 @@ -282,8 +166,8 @@ first = 1 Thess. 5:14-23 gospel = Matt 17:1-9 [ef-lent-1-thursday] -first = 1 Pet 1:1-7 -gospel = Matt 16:13-19 +first = Ezech 18:1-9 +gospel = Matt 15:21-28 [ef-lent-1-tuesday] first = Isa 55:6-11 @@ -433,6 +317,22 @@ gospel = John 10:22-38 first = Acts 2:1-11. gospel = John 14:23-31. +[ef-pentecost-vigil] +first = Acts 19:1-8. +gospel = John 14:15-21. + +[ef-september-ember-fri] +first = Osee 14:2-10 +gospel = Luke 7:36-50 + +[ef-september-ember-sat] +first = Ezek 1:10-14 +gospel = Matt 9:9-13 + +[ef-september-ember-wed] +first = 2 Esd. 8:1-10 +gospel = Mark 9:16-28 + [ef-septuagesima-sunday-1] first = 1 Cor. 9:24-27; 10:1-5 gospel = Matt 20:1-16 @@ -458,8 +358,8 @@ first = Rom 12:16-21 gospel = Matt 8:1-13 [ef-time-after-epiphany-sunday-4] -first = Mal 3:1-4 -gospel = Luke 2:22-32 +first = Rom 13:8-10 +gospel = Matt 8:23-27 [ef-time-after-epiphany-sunday-5] first = Col 3:12-17 diff --git a/internal/calendar/calendar.go b/internal/calendar/calendar.go index e0f5217..99a24b3 100644 --- a/internal/calendar/calendar.go +++ b/internal/calendar/calendar.go @@ -29,7 +29,8 @@ func computeEF(date time.Time, sel Selection, layers []Layer) LiturgicalDay { if !ok { continue } - if sameDay(when, date) { + effective := transferIfImpededEF(cel, when) + if sameDay(effective, date) { cands = append(cands, candidate{Cel: cel, Temporal: false, Season: td.Season}) } } @@ -75,6 +76,13 @@ func computeOF(date time.Time, sel Selection, layers []Layer) LiturgicalDay { cands = append(cands, candidate{Cel: cel, Temporal: false, Season: td.Season}) } } + // Movable Marian memorials that have no fixed date (so they cannot live in the + // sanctoral file): Mary, Mother of the Church, and the Immaculate Heart. + for _, mm := range movableMemorialsOF(date.Year()) { + if sameDay(mm.when, date) { + cands = append(cands, candidate{Cel: mm.cel, Temporal: false, Season: td.Season}) + } + } obs, others := pick(cands) day := LiturgicalDay{ @@ -104,6 +112,31 @@ func celebrationDate(cel Celebration, year int, sel Selection) (time.Time, bool) return resolveDate(cel.Date, year, Easter(year)) } +// movableCel is a celebration whose date is computed (relative to Easter), not +// read from a fixed-date layer. +type movableCel struct { + when time.Time + cel Celebration +} + +// movableMemorialsOF returns the Ordinary Form's two movable Marian memorials, +// which have no fixed calendar date: Mary, Mother of the Church (the Monday after +// Pentecost) and the Immaculate Heart of Mary (the Saturday after the Sacred +// Heart, i.e. easter+69). +func movableMemorialsOF(y int) []movableCel { + e := Easter(y) + mk := func(off int, slug, en, pl string) movableCel { + return movableCel{when: e.AddDate(0, 0, off), cel: Celebration{ + Slug: slug, Rank: RankMemorial, Class: ClassBVM, Colour: White, Layer: "universal", + Name: map[string]string{"en": en, "pl": pl}, + }} + } + return []movableCel{ + mk(50, "mary-mother-of-the-church", "Mary, Mother of the Church", "Najświętszej Maryi Panny, Matki Kościoła"), + mk(69, "the-immaculate-heart-of-the-blessed-virgin-mary", "The Immaculate Heart of the Blessed Virgin Mary", "Niepokalanego Serca Najświętszej Maryi Panny"), + } +} + // transferIfImpeded moves a solemnity off a band-1/2 temporal day to the next // free day. Non-solemnities are never transferred (they yield instead). func transferIfImpeded(cel Celebration, when time.Time, sel Selection) time.Time { @@ -126,6 +159,33 @@ func transferIfImpeded(cel Celebration, when time.Time, sel Selection) time.Time return day } +// transferIfImpededEF moves an EF sanctoral feast off a date it cannot be kept +// on. Two rules cover the 1962 cases: All Souls, when Nov 2 is a Sunday, is +// transferred to the Monday (Nov 3); and a I class feast impeded by a I class +// temporal day (Holy Week, the Easter octave) is pushed forward to the next free +// day — for the Annunciation in Holy Week this lands on the Monday after Low +// Sunday, past the whole privileged octave. Lower-rank feasts are only +// commemorated, never transferred. +func transferIfImpededEF(cel Celebration, when time.Time) time.Time { + if when.Month() == time.November && when.Day() == 2 && when.Weekday() == time.Sunday && + strings.Contains(cel.Slug, "all-souls") { + return when.AddDate(0, 0, 1) + } + if cel.Rank != RankClass1 { + return when + } + day := when + for i := 0; i < 30; i++ { + b := temporalEF(day) + if precedenceEF(candidate{Cel: b.Cel, Temporal: true, Season: b.Season}) <= 2 { + day = day.AddDate(0, 0, 1) // impeded by a I class temporal day; push forward + continue + } + return day + } + return day +} + // buildCelebration types a RawCelebration. func buildCelebration(slug string, rc RawCelebration) Celebration { c := Celebration{Slug: strings.TrimSpace(slug), Name: map[string]string{}, Layer: rc.Fields["layer"]} diff --git a/internal/calendar/coverage_test.go b/internal/calendar/coverage_test.go new file mode 100644 index 0000000..e6555b9 --- /dev/null +++ b/internal/calendar/coverage_test.go @@ -0,0 +1,75 @@ +package calendar_test + +import ( + "strings" + "testing" + "time" + + "github.com/lukaszkasprzak/lectio/internal/caldata" + "github.com/lukaszkasprzak/lectio/internal/calendar" +) + +func day(y int, m time.Month, d int) time.Time { + return time.Date(y, m, d, 0, 0, 0, 0, time.UTC) +} + +// TestEFCoverage checks the EF special days added for full coverage: the movable +// vigils, the September/Advent Ember days, and the transfer of an impeded feast. +// (2027: Easter is March 28.) +func TestEFCoverage(t *testing.T) { + sel := calendar.DefaultSelection() + sel.Form = "old" + layers := []calendar.Layer{caldata.Tridentine()} + + cases := []struct { + date time.Time + slug string + rank calendar.Rank + }{ + {day(2027, time.September, 22), "ef-september-ember-wed", calendar.RankClass2}, + {day(2027, time.December, 15), "ef-advent-ember-wed", calendar.RankClass2}, + {day(2027, time.May, 5), "ef-ascension-vigil", calendar.RankClass2}, + {day(2027, time.May, 15), "ef-pentecost-vigil", calendar.RankClass1}, + {day(2027, time.June, 23), "vigil-of-the-nativity-of-st-john-the-baptist", calendar.RankClass2}, + // Annunciation (Mar 25) is in Holy Week in 2027, so it is transferred to + // the Monday after Low Sunday (Apr 5); Mar 25 itself keeps the Triduum. + {day(2027, time.April, 5), "annunciation-of-the-blessed-virgin-mary", calendar.RankClass1}, + } + for _, c := range cases { + got := calendar.Compute(c.date, sel, layers) + if got.Observed.Slug != c.slug || got.Observed.Rank != c.rank { + t.Errorf("%s: observed %q/%v, want %q/%v", c.date.Format("2006-01-02"), + got.Observed.Slug, got.Observed.Rank, c.slug, c.rank) + } + } + // The Annunciation must NOT also appear on its impeded fixed date. + if got := calendar.Compute(day(2027, time.March, 25), sel, layers); strings.Contains(got.Observed.Slug, "annunciation") { + t.Errorf("2027-03-25 (Holy Thursday) wrongly observes %q", got.Observed.Slug) + } + // All Souls: Nov 2 2025 is a Sunday, so it is transferred to Nov 3. + if got := calendar.Compute(day(2025, time.November, 3), sel, layers); got.Observed.Slug != "commemoration-of-all-souls" { + t.Errorf("2025-11-03: observed %q, want the transferred All Souls", got.Observed.Slug) + } +} + +// TestOFMovableMemorials checks Mary, Mother of the Church, the Immaculate Heart, +// and the two-obligatory-memorials rule. +func TestOFMovableMemorials(t *testing.T) { + sel := calendar.DefaultSelection() // Ordinary Form + layers := []calendar.Layer{caldata.Universal()} + + // Mary, Mother of the Church: Monday after Pentecost (2027: May 17). + if got := calendar.Compute(day(2027, time.May, 17), sel, layers); got.Observed.Slug != "mary-mother-of-the-church" { + t.Errorf("2027-05-17: observed %q, want mary-mother-of-the-church", got.Observed.Slug) + } + // Two obligatory memorials coincide (St Anthony of Padua + the Immaculate + // Heart, 2026-06-13): both become optional and the ferial is observed. + if got := calendar.Compute(day(2026, time.June, 13), sel, layers); got.Observed.Rank != calendar.RankFerial { + t.Errorf("2026-06-13: observed %q/%v, want the ferial", got.Observed.Slug, got.Observed.Rank) + } + // But Mary, Mother of the Church keeps precedence over a coinciding memorial + // (2028-06-05, with St Boniface). + if got := calendar.Compute(day(2028, time.June, 5), sel, layers); got.Observed.Slug != "mary-mother-of-the-church" { + t.Errorf("2028-06-05: observed %q, want mary-mother-of-the-church", got.Observed.Slug) + } +} diff --git a/internal/calendar/precedence.go b/internal/calendar/precedence.go index 88cc0d8..3b7c0a6 100644 --- a/internal/calendar/precedence.go +++ b/internal/calendar/precedence.go @@ -84,5 +84,36 @@ func pick(cands []candidate) (candidate, []candidate) { } return sorted[i].Cel.Slug < sorted[j].Cel.Slug }) + // General Norms 60: when two obligatory memorials fall on the same day, both + // become optional and the weekday (ferial) is celebrated instead -- UNLESS one + // is Mary, Mother of the Church, whose 2018 institution decree gives it + // precedence over a coinciding obligatory memorial (then it is observed). + if len(sorted) >= 2 && !sorted[0].Temporal && !sorted[1].Temporal && + sorted[0].Cel.Rank == RankMemorial && sorted[1].Cel.Rank == RankMemorial { + if i := slugIndex(sorted, "mary-mother-of-the-church"); i >= 0 { + motc := sorted[i] + sorted = append(sorted[:i], sorted[i+1:]...) + sorted = append([]candidate{motc}, sorted...) + } else { + for i, c := range sorted { + if c.Temporal { + ferial := sorted[i] + sorted = append(sorted[:i], sorted[i+1:]...) + sorted = append([]candidate{ferial}, sorted...) + break + } + } + } + } return sorted[0], sorted[1:] } + +// slugIndex returns the position of the candidate with the given slug, or -1. +func slugIndex(cands []candidate, slug string) int { + for i, c := range cands { + if c.Cel.Slug == slug { + return i + } + } + return -1 +} diff --git a/internal/calendar/temporal_ef.go b/internal/calendar/temporal_ef.go index 2ead3e3..877545c 100644 --- a/internal/calendar/temporal_ef.go +++ b/internal/calendar/temporal_ef.go @@ -114,6 +114,10 @@ func temporalEF(date time.Time) temporalDay { 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. @@ -146,12 +150,52 @@ func temporalEF(date time.Time) temporalDay { // the temporal office win, matching the 1962 occurrence rules. rank = RankClass1 } + // The Ember Days of September and Advent (Wednesday, Friday, Saturday after + // the third Sunday of the month/season) are II class privileged ferias in + // violet; they displace a III class saint (only commemorated). The Lenten + // Ember days are already III class Lenten ferias, and the Whitsun Ember days + // fall inside the I class Pentecost octave handled above. + if es, ok := efEmberSlug(date, y); 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. return efCel(season, "ef-"+string(season)+"-"+strconv.Itoa(week)+"-"+weekdayLower(date), col, rank, week) } +// 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 or Advent (the day after the third Sunday + +// 3/5/6). Returns ("", false) otherwise. +func efEmberSlug(date time.Time, y int) (string, bool) { + for _, e := range []struct { + third time.Time + season string + }{ + {thirdSundayOfSeptember(y), "september"}, + {adventStart(y).AddDate(0, 0, 14), "advent"}, // 3rd Sunday of Advent + } { + switch { + case sameDay(date, e.third.AddDate(0, 0, 3)): + return "ef-" + e.season + "-ember-wed", true + case sameDay(date, e.third.AddDate(0, 0, 5)): + return "ef-" + e.season + "-ember-fri", true + case sameDay(date, e.third.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()) } diff --git a/internal/config/config.go b/internal/config/config.go index d4de9c4..91944df 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -63,7 +63,7 @@ const configHeader = `# lectio configuration (INI). Full-line comments only (# o // Version is lectio's release version, shared by every binary's // -v/--version output (lectio, lectio-ui, lectio-web). -const Version = "0.38.0" +const Version = "0.39.0" // validVersions are the five scripture versions lectio understands. var validVersions = map[string]bool{ diff --git a/scripts/gen-sanctoral-ef.go b/scripts/gen-sanctoral-ef.go index f40fe34..e3ad18e 100644 --- a/scripts/gen-sanctoral-ef.go +++ b/scripts/gen-sanctoral-ef.go @@ -133,11 +133,16 @@ func classOf(en string) string { return "" } -// isSaintTitle rejects the temporal commemorations that share the sancti: -// namespace (octave days, vigils) so only genuine saints/feasts are harvested. +// isSaintTitle rejects the purely temporal offices that share the sancti: +// namespace (octave days, ferias, Ember days) so only genuine celebrations are +// harvested. Vigils of fixed feasts (Christmas, the Nativity of St John the +// Baptist, Sts Peter & Paul, St Lawrence, the Assumption, All Saints, the +// Immaculate Conception) ARE kept: they are proper liturgical days on a fixed +// date. The movable vigils (Ascension, Pentecost) live in the tempora namespace +// and are computed by the temporal engine, not harvested here. func isSaintTitle(t string) bool { l := strings.ToLower(t) - for _, bad := range []string{"octave", "vigil", "feria", "ember", "rogation", "sunday", "within the"} { + for _, bad := range []string{"octave", "feria", "ember", "rogation", "sunday", "within the"} { if strings.Contains(l, bad) { return false } diff --git a/scripts/genlect.go b/scripts/genlect.go index 85ff1c2..6d7a43b 100644 --- a/scripts/genlect.go +++ b/scripts/genlect.go @@ -100,7 +100,11 @@ func main() { } slug := day.Observed.Slug isSunday := d.Weekday() == time.Sunday - if !isSunday && !properFerial[day.Season] { + // 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] { |
