Don't commemorate a Vigil alongside the very feast it anticipates
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 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,16 @@ export interface SaintRecord {
|
|||||||
/** See calendar/types.ts's OctaveConfig doc comment. Absent for the vast
|
/** See calendar/types.ts's OctaveConfig doc comment. Absent for the vast
|
||||||
* majority of saints — only ones with a real octave declare it. */
|
* majority of saints — only ones with a real octave declare it. */
|
||||||
octave?: OctaveConfig;
|
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<string, string[]> };
|
const sanctoralCalendar = sanctoralCalendarData as { days: Record<string, string[]> };
|
||||||
|
|||||||
+34
-1
@@ -13,6 +13,7 @@ import { resolveDay } from './index';
|
|||||||
import { addDays } from './date-math';
|
import { addDays } from './date-math';
|
||||||
import { easterOffsetOf } from './temporal';
|
import { easterOffsetOf } from './temporal';
|
||||||
import { isAtLeast } from './commemorations';
|
import { isAtLeast } from './commemorations';
|
||||||
|
import { getSaintRecord } from './feasts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixed/movable "feasts of the Lord" load-bearing elsewhere in this app
|
* 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' } };
|
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
|
/** The ordinary anticipation path: tag tomorrow's identity, then layer in
|
||||||
* today's commemoration if it's eligible (see commemorationOf). Not used
|
* today's commemoration if it's eligible (see commemorationOf). Not used
|
||||||
* by the isMajorFixedFeastOfTheLord override below — whether Christmas/
|
* 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. */
|
* commemorations.ts), so it deliberately doesn't inherit this behavior. */
|
||||||
function anticipated(today: LiturgicalDay, tomorrow: LiturgicalDay): LiturgicalDay {
|
function anticipated(today: LiturgicalDay, tomorrow: LiturgicalDay): LiturgicalDay {
|
||||||
const tagged = tagVespersFrom(tomorrow);
|
const tagged = tagVespersFrom(tomorrow);
|
||||||
const commemoration = commemorationOf(today.winner);
|
const commemoration = isVigilOfTomorrow(today, tomorrow) ? undefined : commemorationOf(today.winner);
|
||||||
if (!commemoration) {
|
if (!commemoration) {
|
||||||
return tagged;
|
return tagged;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
id: vigil-of-all-saints
|
id: vigil-of-all-saints
|
||||||
name: "Vigil of All Saints"
|
name: "Vigil of All Saints"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: all-saints
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: "vigil-of-all-saints"
|
propers: "vigil-of-all-saints"
|
||||||
# P/T/S/N: not confirmed via this pass's own file parser (this vigil's
|
# P/T/S/N: not confirmed via this pass's own file parser (this vigil's
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
id: vigil-of-ss-simon-and-jude
|
id: vigil-of-ss-simon-and-jude
|
||||||
name: "Vigil of Ss. Simon and Jude"
|
name: "Vigil of Ss. Simon and Jude"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: ss-simon-and-jude
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: "vigil-of-ss-simon-and-jude"
|
propers: "vigil-of-ss-simon-and-jude"
|
||||||
# P/T/S/N: no [Ant Prima]/etc of its own in the source (10-27.txt) --
|
# P/T/S/N: no [Ant Prima]/etc of its own in the source (10-27.txt) --
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
id: vigil-of-st-andrew
|
id: vigil-of-st-andrew
|
||||||
name: "Vigil of St. Andrew"
|
name: "Vigil of St. Andrew"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: st-andrew
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: null
|
propers: null
|
||||||
collectCommon: collect-c1v
|
collectCommon: collect-c1v
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
id: vigil-of-st-bartholomew
|
id: vigil-of-st-bartholomew
|
||||||
name: "Vigil of St. Bartholomew"
|
name: "Vigil of St. Bartholomew"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: st-bartholomew
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: null
|
propers: null
|
||||||
collectCommon: collect-c1v
|
collectCommon: collect-c1v
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
id: vigil-of-st-james
|
id: vigil-of-st-james
|
||||||
name: "Vigil of St. James the Greater"
|
name: "Vigil of St. James the Greater"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: st-james-the-greater
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: null
|
propers: null
|
||||||
collectCommon: collect-c1v
|
collectCommon: collect-c1v
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
id: vigil-of-st-john-the-baptist
|
id: vigil-of-st-john-the-baptist
|
||||||
name: "Vigil of St. John the Baptist"
|
name: "Vigil of St. John the Baptist"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: nativity-of-st-john-the-baptist
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: "vigil-of-st-john-the-baptist"
|
propers: "vigil-of-st-john-the-baptist"
|
||||||
# P/T/S/N: not confirmed via this pass's own file parser (this vigil's
|
# P/T/S/N: not confirmed via this pass's own file parser (this vigil's
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
id: vigil-of-st-lawrence
|
id: vigil-of-st-lawrence
|
||||||
name: "Vigil of St. Lawrence"
|
name: "Vigil of St. Lawrence"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: st-lawrence
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: "vigil-of-st-lawrence"
|
propers: "vigil-of-st-lawrence"
|
||||||
# Benedictus antiphon live-verified (2026-08, Lauds query, 2025-08-09, winner
|
# Benedictus antiphon live-verified (2026-08, Lauds query, 2025-08-09, winner
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
id: vigil-of-st-matthew
|
id: vigil-of-st-matthew
|
||||||
name: "Vigil of St. Matthew"
|
name: "Vigil of St. Matthew"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: st-matthew
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: null
|
propers: null
|
||||||
collectCommon: collect-c1v
|
collectCommon: collect-c1v
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
id: vigil-of-st-matthias
|
id: vigil-of-st-matthias
|
||||||
name: "Vigil of St. Matthias"
|
name: "Vigil of St. Matthias"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: st-matthias
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: null
|
propers: null
|
||||||
collectCommon: collect-c1v
|
collectCommon: collect-c1v
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
id: vigil-of-st-thomas
|
id: vigil-of-st-thomas
|
||||||
name: "Vigil of St. Thomas"
|
name: "Vigil of St. Thomas"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: st-thomas-apostle
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: null
|
propers: null
|
||||||
collectCommon: collect-c1v
|
collectCommon: collect-c1v
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
id: vigil-of-the-assumption
|
id: vigil-of-the-assumption
|
||||||
name: "Vigil of the Assumption of the Blessed Virgin Mary"
|
name: "Vigil of the Assumption of the Blessed Virgin Mary"
|
||||||
rank: vigil
|
rank: vigil
|
||||||
|
vigilOf: assumption
|
||||||
common: common-of-a-vigil
|
common: common-of-a-vigil
|
||||||
propers: "vigil-of-the-assumption"
|
propers: "vigil-of-the-assumption"
|
||||||
benedictusCommon: common-of-a-vigil
|
benedictusCommon: common-of-a-vigil
|
||||||
|
|||||||
@@ -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', () => {
|
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
|
// Nov 30, 2025 is Advent I Sunday -- also St. Andrew's own day
|
||||||
// (Duplex II. classis), correctly commemorated alongside it since the
|
// (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 eve = resolveOrdo('compline', '2025-11-29');
|
||||||
const last = eve.parts[eve.parts.length - 1];
|
const last = eve.parts[eve.parts.length - 1];
|
||||||
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');
|
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');
|
||||||
|
|||||||
Reference in New Issue
Block a user