From 79372fb274a55714ddcbed5fed77c1715b08bf25 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Tue, 18 Aug 2026 11:00:09 -0400 Subject: [PATCH] Don't commemorate a Vigil alongside the very feast it anticipates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At Vespers/Compline, a day's own Vigil losing tonight to tomorrow's First Vespers was showing up as a redundant extra commemoration alongside that same feast (e.g. Nov 29's Compline showing both "St. Andrew" and "Vigil of St. Andrew" for Advent I's anticipated evening). Added an explicit `vigilOf` field to each Vigil's own saint record (several Vigil ids don't map cleanly to their feast's id by stripping a `vigil-of-` prefix — `vigil-of-the-assumption` vs `assumption`, `vigil-of-st-james` vs `st-james-the-greater` — so this needs to be explicit rather than guessed from the id or date adjacency). calendar/vespers.ts's evening-anticipation merge now skips adding the Vigil commemoration when tomorrow's winner or its own commemorations already include that same feast. Co-Authored-By: Claude Sonnet 5 --- src/calendar/feasts.ts | 10 ++++++ src/calendar/vespers.ts | 35 ++++++++++++++++++- .../calendar/saints/vigil-of-all-saints.yml | 1 + .../saints/vigil-of-ss-simon-and-jude.yml | 1 + .../calendar/saints/vigil-of-st-andrew.yml | 1 + .../saints/vigil-of-st-bartholomew.yml | 1 + .../calendar/saints/vigil-of-st-james.yml | 1 + .../saints/vigil-of-st-john-the-baptist.yml | 1 + .../calendar/saints/vigil-of-st-lawrence.yml | 1 + .../calendar/saints/vigil-of-st-matthew.yml | 1 + .../calendar/saints/vigil-of-st-matthias.yml | 1 + .../calendar/saints/vigil-of-st-thomas.yml | 1 + .../saints/vigil-of-the-assumption.yml | 1 + tests/hours/compline.test.ts | 7 +++- 14 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/calendar/feasts.ts b/src/calendar/feasts.ts index fa14375..ec43cff 100644 --- a/src/calendar/feasts.ts +++ b/src/calendar/feasts.ts @@ -78,6 +78,16 @@ export interface SaintRecord { /** See calendar/types.ts's OctaveConfig doc comment. Absent for the vast * majority of saints — only ones with a real octave declare it. */ octave?: OctaveConfig; + /** For a `rank: vigil` record only — the real id of the feast this is + * the eve of (e.g. `st-andrew`, not derivable from this record's own id + * by stripping a `vigil-of-` prefix: several vigils' ids don't match + * their feast's id exactly — `vigil-of-the-assumption` vs `assumption`, + * `vigil-of-st-james` vs `st-james-the-greater`). Lets + * calendar/vespers.ts's evening-anticipation merge recognize "today's + * Vigil is being displaced by the very feast it's the eve of" precisely + * (rather than guessing from date adjacency alone) and skip + * commemorating the Vigil redundantly alongside its own feast. */ + vigilOf?: string; } const sanctoralCalendar = sanctoralCalendarData as { days: Record }; diff --git a/src/calendar/vespers.ts b/src/calendar/vespers.ts index 3071c93..941d311 100644 --- a/src/calendar/vespers.ts +++ b/src/calendar/vespers.ts @@ -13,6 +13,7 @@ import { resolveDay } from './index'; import { addDays } from './date-math'; import { easterOffsetOf } from './temporal'; import { isAtLeast } from './commemorations'; +import { getSaintRecord } from './feasts'; /** * Fixed/movable "feasts of the Lord" load-bearing elsewhere in this app @@ -95,6 +96,38 @@ function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay { return { ...tomorrow, winner: { ...tomorrow.winner, vespersFrom: 'firstVespersOfTomorrow' } }; } +/** Today's own Vigil isn't commemorated alongside the very feast it's the + * eve of — e.g. Nov 29's Vigil of St. Andrew shouldn't also show up next + * to St. Andrew himself once this evening has already adopted his First + * Vespers, whether St. Andrew governs it outright or is merely + * commemorated there himself (2025-11-30: he's only commemorated under + * Advent I, a `privileged-sunday`, per commemorations.ts's own threshold + * — the Vigil would otherwise show up redundantly alongside him even + * though neither one is tomorrow's actual winner). Matched via each + * Vigil's own explicit `vigilOf` field (calendar/feasts.ts's + * `SaintRecord`) against tomorrow's winner *and* its commemorations, not + * date adjacency or name-guessing — several vigils' own ids don't map + * cleanly to their feast's id (`vigil-of-the-assumption` vs `assumption`, + * `vigil-of-st-james` vs `st-james-the-greater`), so this needs the + * explicit link. Scoped to this evening-anticipation merge specifically, + * not `commemorationOf` itself — the reverse direction + * (`keepsOwnSecondVespers`, commemorating *tomorrow's* Vigil while today + * keeps its own Vespers) can't hit this collision: a Vigil dated tomorrow + * belongs to a feast the day after tomorrow, never today. */ +function isVigilOfTomorrow(today: LiturgicalDay, tomorrow: LiturgicalDay): boolean { + if (today.winner.kind !== 'sanctoral' || today.winner.rank !== 'vigil') { + return false; + } + const feastId = getSaintRecord(today.winner.id)?.vigilOf; + if (!feastId) { + return false; + } + return ( + (tomorrow.winner.kind === 'sanctoral' && tomorrow.winner.id === feastId) || + tomorrow.commemorations.some((c) => c.kind === 'sanctoral' && c.id === feastId) + ); +} + /** The ordinary anticipation path: tag tomorrow's identity, then layer in * today's commemoration if it's eligible (see commemorationOf). Not used * by the isMajorFixedFeastOfTheLord override below — whether Christmas/ @@ -104,7 +137,7 @@ function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay { * commemorations.ts), so it deliberately doesn't inherit this behavior. */ function anticipated(today: LiturgicalDay, tomorrow: LiturgicalDay): LiturgicalDay { const tagged = tagVespersFrom(tomorrow); - const commemoration = commemorationOf(today.winner); + const commemoration = isVigilOfTomorrow(today, tomorrow) ? undefined : commemorationOf(today.winner); if (!commemoration) { return tagged; } diff --git a/src/data/calendar/saints/vigil-of-all-saints.yml b/src/data/calendar/saints/vigil-of-all-saints.yml index 9ec6eba..72c4d98 100644 --- a/src/data/calendar/saints/vigil-of-all-saints.yml +++ b/src/data/calendar/saints/vigil-of-all-saints.yml @@ -4,6 +4,7 @@ id: vigil-of-all-saints name: "Vigil of All Saints" rank: vigil +vigilOf: all-saints common: common-of-a-vigil propers: "vigil-of-all-saints" # P/T/S/N: not confirmed via this pass's own file parser (this vigil's diff --git a/src/data/calendar/saints/vigil-of-ss-simon-and-jude.yml b/src/data/calendar/saints/vigil-of-ss-simon-and-jude.yml index cf59b8d..85d744b 100644 --- a/src/data/calendar/saints/vigil-of-ss-simon-and-jude.yml +++ b/src/data/calendar/saints/vigil-of-ss-simon-and-jude.yml @@ -3,6 +3,7 @@ id: vigil-of-ss-simon-and-jude name: "Vigil of Ss. Simon and Jude" rank: vigil +vigilOf: ss-simon-and-jude common: common-of-a-vigil propers: "vigil-of-ss-simon-and-jude" # P/T/S/N: no [Ant Prima]/etc of its own in the source (10-27.txt) -- diff --git a/src/data/calendar/saints/vigil-of-st-andrew.yml b/src/data/calendar/saints/vigil-of-st-andrew.yml index c864c4c..3e843c8 100644 --- a/src/data/calendar/saints/vigil-of-st-andrew.yml +++ b/src/data/calendar/saints/vigil-of-st-andrew.yml @@ -6,6 +6,7 @@ id: vigil-of-st-andrew name: "Vigil of St. Andrew" rank: vigil +vigilOf: st-andrew common: common-of-a-vigil propers: null collectCommon: collect-c1v diff --git a/src/data/calendar/saints/vigil-of-st-bartholomew.yml b/src/data/calendar/saints/vigil-of-st-bartholomew.yml index 184c16b..b1f0052 100644 --- a/src/data/calendar/saints/vigil-of-st-bartholomew.yml +++ b/src/data/calendar/saints/vigil-of-st-bartholomew.yml @@ -8,6 +8,7 @@ id: vigil-of-st-bartholomew name: "Vigil of St. Bartholomew" rank: vigil +vigilOf: st-bartholomew common: common-of-a-vigil propers: null collectCommon: collect-c1v diff --git a/src/data/calendar/saints/vigil-of-st-james.yml b/src/data/calendar/saints/vigil-of-st-james.yml index 692bc50..d12db87 100644 --- a/src/data/calendar/saints/vigil-of-st-james.yml +++ b/src/data/calendar/saints/vigil-of-st-james.yml @@ -6,6 +6,7 @@ id: vigil-of-st-james name: "Vigil of St. James the Greater" rank: vigil +vigilOf: st-james-the-greater common: common-of-a-vigil propers: null collectCommon: collect-c1v diff --git a/src/data/calendar/saints/vigil-of-st-john-the-baptist.yml b/src/data/calendar/saints/vigil-of-st-john-the-baptist.yml index a7ed77f..887eaed 100644 --- a/src/data/calendar/saints/vigil-of-st-john-the-baptist.yml +++ b/src/data/calendar/saints/vigil-of-st-john-the-baptist.yml @@ -6,6 +6,7 @@ id: vigil-of-st-john-the-baptist name: "Vigil of St. John the Baptist" rank: vigil +vigilOf: nativity-of-st-john-the-baptist common: common-of-a-vigil propers: "vigil-of-st-john-the-baptist" # P/T/S/N: not confirmed via this pass's own file parser (this vigil's diff --git a/src/data/calendar/saints/vigil-of-st-lawrence.yml b/src/data/calendar/saints/vigil-of-st-lawrence.yml index ff212dd..7474bad 100644 --- a/src/data/calendar/saints/vigil-of-st-lawrence.yml +++ b/src/data/calendar/saints/vigil-of-st-lawrence.yml @@ -6,6 +6,7 @@ id: vigil-of-st-lawrence name: "Vigil of St. Lawrence" rank: vigil +vigilOf: st-lawrence common: common-of-a-vigil propers: "vigil-of-st-lawrence" # Benedictus antiphon live-verified (2026-08, Lauds query, 2025-08-09, winner diff --git a/src/data/calendar/saints/vigil-of-st-matthew.yml b/src/data/calendar/saints/vigil-of-st-matthew.yml index 5b2ddd9..f3de616 100644 --- a/src/data/calendar/saints/vigil-of-st-matthew.yml +++ b/src/data/calendar/saints/vigil-of-st-matthew.yml @@ -13,6 +13,7 @@ id: vigil-of-st-matthew name: "Vigil of St. Matthew" rank: vigil +vigilOf: st-matthew common: common-of-a-vigil propers: null collectCommon: collect-c1v diff --git a/src/data/calendar/saints/vigil-of-st-matthias.yml b/src/data/calendar/saints/vigil-of-st-matthias.yml index 0f053f7..0724aff 100644 --- a/src/data/calendar/saints/vigil-of-st-matthias.yml +++ b/src/data/calendar/saints/vigil-of-st-matthias.yml @@ -7,6 +7,7 @@ id: vigil-of-st-matthias name: "Vigil of St. Matthias" rank: vigil +vigilOf: st-matthias common: common-of-a-vigil propers: null collectCommon: collect-c1v diff --git a/src/data/calendar/saints/vigil-of-st-thomas.yml b/src/data/calendar/saints/vigil-of-st-thomas.yml index a9aac12..51115b2 100644 --- a/src/data/calendar/saints/vigil-of-st-thomas.yml +++ b/src/data/calendar/saints/vigil-of-st-thomas.yml @@ -11,6 +11,7 @@ id: vigil-of-st-thomas name: "Vigil of St. Thomas" rank: vigil +vigilOf: st-thomas-apostle common: common-of-a-vigil propers: null collectCommon: collect-c1v diff --git a/src/data/calendar/saints/vigil-of-the-assumption.yml b/src/data/calendar/saints/vigil-of-the-assumption.yml index 565ea24..a5b7876 100644 --- a/src/data/calendar/saints/vigil-of-the-assumption.yml +++ b/src/data/calendar/saints/vigil-of-the-assumption.yml @@ -6,6 +6,7 @@ id: vigil-of-the-assumption name: "Vigil of the Assumption of the Blessed Virgin Mary" rank: vigil +vigilOf: assumption common: common-of-a-vigil propers: "vigil-of-the-assumption" benedictusCommon: common-of-a-vigil diff --git a/tests/hours/compline.test.ts b/tests/hours/compline.test.ts index 6448cbf..c0cc04e 100644 --- a/tests/hours/compline.test.ts +++ b/tests/hours/compline.test.ts @@ -88,7 +88,12 @@ describe('resolveOrdo("compline", ...)', () => { it('anticipates Advent I: the Saturday evening before switches to the Alma Redemptoris Mater and shows the anticipated day label', () => { // Nov 30, 2025 is Advent I Sunday -- also St. Andrew's own day // (Duplex II. classis), correctly commemorated alongside it since the - // November sanctoral pull (see tests/calendar/day-label.test.ts). + // November sanctoral pull (see tests/calendar/day-label.test.ts). This + // evening also anticipates Advent I from Nov 29, itself the Vigil of + // St. Andrew -- but the Vigil isn't also shown alongside his own + // feast (calendar/vespers.ts's own isVigilOfTomorrow check, keyed off + // each Vigil's explicit `vigilOf` field): showing both would just be + // redundant, the same feast named twice. const eve = resolveOrdo('compline', '2025-11-29'); const last = eve.parts[eve.parts.length - 1]; expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');