From 25aa1fa687ec714e145a998714c29a5f5728a603 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 18 Aug 2026 16:35:41 +0200 Subject: feat(ef): Rogation Monday and Tuesday (RG 87) RG 87: "Litaniae minores seu Rogationes, per se, assignantur feriis II, III et IV ante festum Ascensionis Domini." Easter+36 and +37 are that Monday and Tuesday. RG 128(d) gives them violet -- "for the procession and Mass of the Greater and Lesser Litanies" -- a penitential note inside white Paschaltide. lectio computed no Rogation days at all and showed a plain paschaltide feria. The WEDNESDAY is deliberately not built, and the test asserts its absence. Easter+38 is by construction the Vigil of the Ascension, II class, which wins the day outright; what the rubric asks for there is a COMMEMORATION, and this function returns one office per day. colitur reaches it through a movable-date entry in its sanctoral overlay. Extending the Rogation branch to +38 to "complete" the rubric would silently displace the Vigil, so the test pins the Vigil rather than leaving the omission to a comment. Rank is left as the feria's: these are IV-class days and any III-class saint displaces them, which is what makes the office rare. In 2026 all three are impeded and none appears; 2027 has the Monday unimpeded, and lectio and colitur are byte-identical across 2-5 May that year. One oracle allow-list entry: missalemeum builds no Rogation days either and shows a white feria, so it is the outlier here rather than lectio. --- internal/calendar/oracle_ef_test.go | 20 ++++++++++++++++++++ internal/calendar/temporal_ef.go | 25 +++++++++++++++++++++++++ internal/calendar/temporal_ef_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) (limited to 'internal') diff --git a/internal/calendar/oracle_ef_test.go b/internal/calendar/oracle_ef_test.go index b765a34..79b31a5 100644 --- a/internal/calendar/oracle_ef_test.go +++ b/internal/calendar/oracle_ef_test.go @@ -127,6 +127,26 @@ type efAllow struct { } var efAllowList = []efAllow{ + { + // RG 87: "Litaniae minores seu Rogationes, per se, assignantur feriis + // II, III et IV ante festum Ascensionis Domini." The Monday and + // Tuesday before the Ascension are Rogation days, and RG 128(d) gives + // violet to "the procession and Mass of the Greater and Lesser + // Litanies" -- a penitential note inside white Paschaltide. + // + // missalemeum builds no Rogation days and shows a plain white + // paschaltide feria. It is the outlier here, not lectio: colitur has + // had these since its own v0.1 and the two engines now agree day for + // day. Rare in practice, because a III-class saint displaces them -- + // in 2026 all three are impeded and none appears. + match: func(date string, day time.Time, o efOracleDay) bool { + e := calendar.Easter(day.Year()) + off := int(day.Sub(e).Hours() / 24) + return off == 36 || off == 37 + }, + field: "colour", + reason: "RG 87 + RG 128(d): the Rogation Monday and Tuesday before the Ascension are violet; missalemeum builds no Rogation days (colitur C8)", + }, { // RG 124, "De coloribus paramentorum", section C "De colore rubro", // both photographic scans word for word: 124(b) gives red to diff --git a/internal/calendar/temporal_ef.go b/internal/calendar/temporal_ef.go index 7d31b8b..a043179 100644 --- a/internal/calendar/temporal_ef.go +++ b/internal/calendar/temporal_ef.go @@ -276,6 +276,31 @@ func temporalEF(date time.Time) temporalDay { // 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 diff --git a/internal/calendar/temporal_ef_test.go b/internal/calendar/temporal_ef_test.go index 1290a59..e8ef391 100644 --- a/internal/calendar/temporal_ef_test.go +++ b/internal/calendar/temporal_ef_test.go @@ -300,3 +300,37 @@ func TestTemporalEFBVMSaturday(t *testing.T) { } } } + +// TestTemporalEFRogationDays pins RG 87's Minor Litanies: "Litaniae minores seu +// Rogationes, per se, assignantur feriis II, III et IV ante festum Ascensionis +// Domini." Easter+36 and +37 are that Monday and Tuesday; RG 128(d) makes them +// violet, "for the procession and Mass of the Greater and Lesser Litanies" -- +// a penitential note inside white Paschaltide. +// +// The Wednesday is asserted in the opposite direction on purpose. Easter+38 is +// by construction the Vigil of the Ascension, II class, which wins the day; the +// rubric asks for a COMMEMORATION there, which this function cannot express +// since it returns one office per day. A change that extended the Rogation +// branch to +38 to "complete" the rubric would silently displace the Vigil, and +// this test is what stops it. +func TestTemporalEFRogationDays(t *testing.T) { + // 2027: Easter is 28 March, so the Rogation days are 3-5 May and the + // Monday is unimpeded that year. + easter := d("2027-03-28") + for off, want := range map[int]string{36: "ef-rogation-monday", 37: "ef-rogation-tuesday"} { + day := easter.AddDate(0, 0, off) + got := temporalEF(day) + if got.Cel.Slug != want { + t.Errorf("Easter+%d (%s): slug = %q, want %q", off, day.Format("2006-01-02"), got.Cel.Slug, want) + } + if got.Cel.Colour != Violet { + t.Errorf("Easter+%d (%s): colour = %s, want violet (RG 128(d))", + off, day.Format("2006-01-02"), got.Cel.Colour) + } + } + // Easter+38 is the Ascension's Vigil and must stay so. + if got := temporalEF(easter.AddDate(0, 0, 38)); got.Cel.Slug != "ef-ascension-vigil" { + t.Errorf("Easter+38: slug = %q, want ef-ascension-vigil -- the Rogation Wednesday's "+ + "commemoration cannot be built here without displacing the Vigil", got.Cel.Slug) + } +} -- cgit v1.3