Compare commits

..

2 Commits

Author SHA1 Message Date
will a339068f81 Show a transferred saint's status in the day label
Deploy / deploy (push) Successful in 1m32s
getDayLabel now surfaces the transfer signal added in the previous
commit: a transferred-in winner gets "(Rank, transferred)" instead of
just "(Rank)"; a transferred-in commemoration gets "(transferred)"
appended to its bare name; and the date a saint transferred away from
gets a trailing "St. X (transferred away)" note so a reader isn't left
wondering why an expected saint is missing that day. Deliberately worded
differently on the two sides (plain "transferred" reads as "arrived via
transfer" next to a rank parenthetical; "transferred away" spells out the
direction on the origin date, where there's no rank alongside it to
anchor that reading) and deliberately omits a landing date on the away
note, per direct instruction, since a real landing can chain further than
the single adjacent day resolveDay actually checks (an unmodeled case,
e.g. a transfer running into Holy Week).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013g7AsSvMD9oazR17f3BxLz
2026-08-31 08:53:12 -04:00
will db9c844caf Track a transferred saint's origin date on DayWinner and Commemoration
Adds transferredFrom to both DayWinner's sanctoral variant and
Commemoration's sanctoral variant, set in calendar/index.ts's
applyIncomingTransfer whenever a transferred-in candidate actually lands
(wins outright) or ends up merely commemorated (lost a collision, or fell
below the landing day's own threshold) — previously this signal was
discarded after resolveDay used it once to decide the winner, so nothing
downstream could tell a transferred feast apart from a native one.

Also adds transferredAway to LiturgicalDay, set from a date's own native
occurrence when its candidate couldn't be kept there at all, so a UI can
note "this office moved elsewhere" without asserting a specific landing
date (which a later, unmodeled multi-hop chain — e.g. into Holy Week —
could get wrong).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013g7AsSvMD9oazR17f3BxLz
2026-08-31 08:53:02 -04:00
8 changed files with 153 additions and 31 deletions
+58 -11
View File
@@ -443,11 +443,18 @@ function collectCommemorations(
for (const c of day.commemorations) { for (const c of day.commemorations) {
if (c.kind === 'sanctoral') { if (c.kind === 'sanctoral') {
const name = biName(c.name, c.nameLa); const name = biName(c.name, c.nameLa);
sanctoral.push( if (c.vespersNote) {
c.vespersNote sanctoral.push(bi(`${name.en} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].en})`, `${name.la} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].la})`));
? bi(`${name.en} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].en})`, `${name.la} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].la})`) } else if (c.transferredFrom) {
: name, // Same "arrived via transfer, not natively due here" signal as a
); // transferred-in *winner* gets — a reader shouldn't need to already
// know this saint's real date to realize a mere commemoration here
// is standing in for a transfer, not this date's own natural
// sanctoral standing.
sanctoral.push(bi(`${name.en} (${TRANSFERRED_LABEL.en})`, `${name.la} (${TRANSFERRED_LABEL.la})`));
} else {
sanctoral.push(name);
}
} else if (c.kind === 'temporal') { } else if (c.kind === 'temporal') {
if (c.id === temporalId) { if (c.id === temporalId) {
temporal.push(anchorDayName(day, false) ?? temporalLabel(day)); temporal.push(anchorDayName(day, false) ?? temporalLabel(day));
@@ -520,6 +527,44 @@ export function getVespersStatusLabel(day: LiturgicalDay, requestedDate: string)
return bi('Second Vespers (of today)', 'Vesperæ de hodierno'); return bi('Second Vespers (of today)', 'Vesperæ de hodierno');
} }
/** Appended after a transferred-in winner's name/rank, before any
* commemorations — same shape as a rank parenthetical. Distinct wording
* from `TRANSFERRED_AWAY_LABEL` below on purpose: this winner's own rank
* parenthetical sits right next to it ("St. X (Duplex, transferred)"), so
* plain "transferred" already reads as "arrived via transfer" in context —
* but on the origin date there's no rank alongside it to anchor that
* reading, so the away-note spells out the direction explicitly instead of
* relying on that same subtlety a first-time reader wouldn't have. */
const TRANSFERRED_LABEL = bi('transferred', 'translata');
/** See `TRANSFERRED_LABEL`'s doc comment for why this is worded differently. */
const TRANSFERRED_AWAY_LABEL = bi('transferred away', 'translata alio');
/** "St. X (transferred away)" note for a date whose own native sanctoral
* candidate (`day.transferredAway`) couldn't be kept here — appended after
* everything else `getDayLabel` already found for this date's real
* winner/commemorations, since the departure is a footnote about a
* *different* saint, not part of this date's own identity. Deliberately
* names no landing date (see `transferredAway`'s own doc comment) — the
* point is just to tell a reader "not an omission, this office moved
* elsewhere," not to claim a specific destination that a later, unmodeled
* transfer chain could make wrong. */
function transferredAwayNote(day: LiturgicalDay): Bi | undefined {
if (!day.transferredAway) {
return undefined;
}
const name = biName(day.transferredAway.candidate.name, day.transferredAway.candidate.nameLa);
return bi(`${name.en} (${TRANSFERRED_AWAY_LABEL.en})`, `${name.la} (${TRANSFERRED_AWAY_LABEL.la})`);
}
function withTransferNote(day: LiturgicalDay, label: Partial<Record<string, string>>): Partial<Record<string, string>> {
const note = transferredAwayNote(day);
if (!note) {
return label;
}
return { en: `${label.en}${note.en}`, la: `${label.la}${note.la}` };
}
/** /**
* The full "day being celebrated" label: the day's own primary identity * The full "day being celebrated" label: the day's own primary identity
* (a sanctoral winner, a named temporal feast, a season's own anchor day, * (a sanctoral winner, a named temporal feast, a season's own anchor day,
@@ -547,8 +592,10 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
}); });
const rank = formatRank(day.winner.rank); const rank = formatRank(day.winner.rank);
const winnerName = biName(day.winner.name, day.winner.nameLa); const winnerName = biName(day.winner.name, day.winner.nameLa);
const winner = bi(`${winnerName.en} (${rank.en})`, `${winnerName.la} (${rank.la})`); const winner = day.winner.transferredFrom
return joinBi([winner, ...temporal, ...sanctoral, ...octaves]); ? bi(`${winnerName.en} (${rank.en}, ${TRANSFERRED_LABEL.en})`, `${winnerName.la} (${rank.la}, ${TRANSFERRED_LABEL.la})`)
: bi(`${winnerName.en} (${rank.en})`, `${winnerName.la} (${rank.la})`);
return withTransferNote(day, joinBi([winner, ...temporal, ...sanctoral, ...octaves]));
} }
// A named temporal feast (Christmas, Pentecost, Marian Saturday, ...) // A named temporal feast (Christmas, Pentecost, Marian Saturday, ...)
@@ -563,7 +610,7 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
? bi(`${name.en} (${formatRank(namedFeast.rank).en})`, `${name.la} (${formatRank(namedFeast.rank).la})`) ? bi(`${name.en} (${formatRank(namedFeast.rank).en})`, `${name.la} (${formatRank(namedFeast.rank).la})`)
: name; : name;
const { sanctoral } = collectCommemorations(day, { showOctaves: false }); const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([primary, ...sanctoral]); return withTransferNote(day, joinBi([primary, ...sanctoral]));
} }
// A season's own named anchor day (Trinity Sunday, Easter, Ash // A season's own named anchor day (Trinity Sunday, Easter, Ash
@@ -576,7 +623,7 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
const anchorName = anchorDayName(day); const anchorName = anchorDayName(day);
if (anchorName) { if (anchorName) {
const { sanctoral } = collectCommemorations(day, { showOctaves: false }); const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([anchorName, ...sanctoral]); return withTransferNote(day, joinBi([anchorName, ...sanctoral]));
} }
// An active octave (St. Lawrence's, ...) is this day's real primary // An active octave (St. Lawrence's, ...) is this day's real primary
@@ -612,12 +659,12 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
if (activeOctave) { if (activeOctave) {
const { sanctoral, octaves } = collectCommemorations(day, { showOctaves: true, excludeOctaveId: activeOctave.id }); const { sanctoral, octaves } = collectCommemorations(day, { showOctaves: true, excludeOctaveId: activeOctave.id });
const primary = octaveLabel(activeOctave); const primary = octaveLabel(activeOctave);
return joinBi([primary, ...octaves, ...sanctoral]); return withTransferNote(day, joinBi([primary, ...octaves, ...sanctoral]));
} }
const temporalRank = TEMPORAL_CATEGORY_RANK_LABELS[day.temporalCategory]; const temporalRank = TEMPORAL_CATEGORY_RANK_LABELS[day.temporalCategory];
const label = temporalLabel(day); const label = temporalLabel(day);
const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`); const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`);
const { sanctoral } = collectCommemorations(day, { showOctaves: false }); const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([temporal, ...sanctoral]); return withTransferNote(day, joinBi([temporal, ...sanctoral]));
} }
+30 -9
View File
@@ -56,6 +56,7 @@ function applyIncomingTransfer(
temporalCategory: TemporalCategory, temporalCategory: TemporalCategory,
winner: DayWinner, winner: DayWinner,
commemorations: Commemoration[], commemorations: Commemoration[],
fromDate: string,
): DayWinner { ): DayWinner {
if (temporalCategory === 'privileged-feria-major') { if (temporalCategory === 'privileged-feria-major') {
// Deferred: a transfer landing on one of these (the concrete case is // Deferred: a transfer landing on one of these (the concrete case is
@@ -67,6 +68,18 @@ function applyIncomingTransfer(
// ordinary-sunday landing. // ordinary-sunday landing.
return winner; return winner;
} }
// Tags the returned winner with where it transferred from, but only when
// the candidate actually ended up winning here — a collision loss (the
// candidate merely commemorated, incumbent keeps the day) isn't a landing.
const tagIfLanded = (result: DayWinner): DayWinner =>
result.kind === 'sanctoral' && result.id === candidate.id ? { ...result, transferredFrom: fromDate } : result;
// Same tagging, but for the candidate's own *commemoration* — it still
// needs marking as arrived-via-transfer even when it lost the landing
// day outright (a collision loss, or falling below the landing day's own
// threshold in decideOccurrence) rather than merely being naturally due
// for commemoration here.
const tagCommemorations = (result: Commemoration[]): Commemoration[] =>
result.map((c) => (c.kind === 'sanctoral' && c.id === candidate.id ? { ...c, transferredFrom: fromDate } : c));
if (winner.kind === 'temporal') { if (winner.kind === 'temporal') {
// Found via real data: a transferred-in Simplex saint was blindly // Found via real data: a transferred-in Simplex saint was blindly
// winning outright even when it landed on a privileged Sunday (Trinity // winning outright even when it landed on a privileged Sunday (Trinity
@@ -74,15 +87,15 @@ function applyIncomingTransfer(
// the *same* precedence rules a native occurrence would have used, // the *same* precedence rules a native occurrence would have used,
// not just take over because nothing else was assigned here. // not just take over because nothing else was assigned here.
const decided = decideOccurrence(temporalCategory, winner.id, candidate); const decided = decideOccurrence(temporalCategory, winner.id, candidate);
commemorations.push(...decided.commemorations); commemorations.push(...tagCommemorations(decided.commemorations));
// If the candidate fails here too (decided.transfer set), that's a // If the candidate fails here too (decided.transfer set), that's a
// transfer chain — not modeled, same "not delivered rather than // transfer chain — not modeled, same "not delivered rather than
// guessed at" stance as the privileged-feria-major case above. // guessed at" stance as the privileged-feria-major case above.
return decided.winner; return tagIfLanded(decided.winner);
} }
const collision = resolveCollision(candidate, winner); const collision = resolveCollision(candidate, winner);
commemorations.push(...collision.commemorations); commemorations.push(...tagCommemorations(collision.commemorations));
return collision.winner; return tagIfLanded(collision.winner);
} }
/** /**
@@ -99,13 +112,15 @@ export function resolveDay(isoDate: string): LiturgicalDay {
let winner = result.winner; let winner = result.winner;
const commemorations = [...result.commemorations]; const commemorations = [...result.commemorations];
const yesterday = resolveNativeOccurrence(addDays(isoDate, -1)); const yesterdayIso = addDays(isoDate, -1);
const yesterday = resolveNativeOccurrence(yesterdayIso);
if (yesterday.result.transfer?.direction === 'forward') { if (yesterday.result.transfer?.direction === 'forward') {
winner = applyIncomingTransfer(yesterday.result.transfer.candidate, temporalCategory, winner, commemorations); winner = applyIncomingTransfer(yesterday.result.transfer.candidate, temporalCategory, winner, commemorations, yesterdayIso);
} }
const tomorrow = resolveNativeOccurrence(addDays(isoDate, 1)); const tomorrowIso = addDays(isoDate, 1);
const tomorrow = resolveNativeOccurrence(tomorrowIso);
if (tomorrow.result.transfer?.direction === 'backward') { if (tomorrow.result.transfer?.direction === 'backward') {
winner = applyIncomingTransfer(tomorrow.result.transfer.candidate, temporalCategory, winner, commemorations); winner = applyIncomingTransfer(tomorrow.result.transfer.candidate, temporalCategory, winner, commemorations, tomorrowIso);
} }
winner = applyOctaves(isoDate, winner, commemorations); winner = applyOctaves(isoDate, winner, commemorations);
@@ -116,7 +131,13 @@ export function resolveDay(isoDate: string): LiturgicalDay {
applyEpiphany6Commemoration(isoDate, commemorations); applyEpiphany6Commemoration(isoDate, commemorations);
applyAdventFourVigilCommemoration(isoDate, weekday, commemorations); applyAdventFourVigilCommemoration(isoDate, weekday, commemorations);
return { date: isoDate, weekday, season, temporalCategory, winner, commemorations }; // This date's own native candidate (if any) couldn't be kept here and
// moved on instead — see calendar/types.ts's `transferredAway` doc
// comment for why this is tracked separately from `Commemoration`, and
// why no landing date is recorded.
const transferredAway = result.transfer ? { candidate: result.transfer.candidate } : undefined;
return { date: isoDate, weekday, season, temporalCategory, winner, commemorations, transferredAway };
} }
/** /**
+26
View File
@@ -161,6 +161,12 @@ export type DayWinner =
// First Vespers is being anticipated this evening (i.e. this feast // First Vespers is being anticipated this evening (i.e. this feast
// belongs to *tomorrow*, but is winning tonight's Vespers/Compline). // belongs to *tomorrow*, but is winning tonight's Vespers/Compline).
vespersFrom?: 'firstVespersOfTomorrow'; vespersFrom?: 'firstVespersOfTomorrow';
// Set by calendar/index.ts's resolveDay/applyIncomingTransfer when
// this winner is only here because its own native date (an adjacent
// ISO date) couldn't hold it (calendar/commemorations.ts's
// `decideOccurrence` `transfer` signal) — the ISO date it transferred
// *from*. Absent for a feast winning on its own native date.
transferredFrom?: string;
} & SanctoralIdentity); } & SanctoralIdentity);
/** /**
@@ -184,6 +190,14 @@ export type Commemoration =
// commemoration (a real collision, an octave day, etc.), which // commemoration (a real collision, an octave day, etc.), which
// carries no such qualifier. // carries no such qualifier.
vespersNote?: 'today' | 'tomorrow'; vespersNote?: 'today' | 'tomorrow';
// Set by calendar/index.ts's applyIncomingTransfer, same meaning as
// `DayWinner`'s own `transferredFrom` — this saint is only
// commemorated here (rather than absent) because a transfer landed
// on this date and lost a collision, or fell below the landing
// day's own outright-winning threshold, not because it's natively
// due for commemoration here. Absent for an ordinary same-day
// commemoration.
transferredFrom?: string;
} & SanctoralIdentity) } & SanctoralIdentity)
| { kind: 'octave'; id: string; name: string; nameLa?: string }; | { kind: 'octave'; id: string; name: string; nameLa?: string };
@@ -199,4 +213,16 @@ export interface LiturgicalDay {
winner: DayWinner; winner: DayWinner;
/** Everything else commemorated alongside the winner — see the doc comment on Commemoration. */ /** Everything else commemorated alongside the winner — see the doc comment on Commemoration. */
commemorations: Commemoration[]; commemorations: Commemoration[];
/** Set when this date's own native sanctoral candidate couldn't be kept
* here at all (calendar/commemorations.ts's `decideOccurrence` `transfer`
* signal) and moved to a later/earlier date instead. No landing date is
* recorded here — `resolveDay` only ever checks the immediate adjacent
* date, but a real landing can chain further than that (an unmodeled
* case noted in `applyIncomingTransfer`, e.g. a transfer running into
* Holy Week), so naming a specific "to" date risked asserting a wrong
* one; this exists purely so the display can say "not an omission, this
* office moved elsewhere" without claiming to know where. Distinct from
* — and not implied by — `Commemoration`, since a transferred-away
* candidate gets no commemoration on its own native date at all. */
transferredAway?: { candidate: SanctoralIdentity };
} }
+8 -1
View File
@@ -45,7 +45,14 @@ describe('April sanctoral pull (first pass)', () => {
]); ]);
const monday = resolveDay('2029-04-30'); const monday = resolveDay('2029-04-30');
expect(monday.winner).toEqual({ kind: 'sanctoral', id: 'st-robert', name: 'St. Robert, Abbot', nameLa: 'Sanctus Robertus, Abbas', rank: 'duplex' }); expect(monday.winner).toEqual({
kind: 'sanctoral',
id: 'st-robert',
name: 'St. Robert, Abbot',
nameLa: 'Sanctus Robertus, Abbas',
rank: 'duplex',
transferredFrom: '2029-04-29',
});
expect(monday.commemorations).toEqual([ expect(monday.commemorations).toEqual([
{ kind: 'sanctoral', id: 'st-peter-martyr', name: 'St. Peter Martyr, Martyr', nameLa: 'Sanctus Petrus Martyr, Martyr', rank: 'semiduplex' }, { kind: 'sanctoral', id: 'st-peter-martyr', name: 'St. Peter Martyr, Martyr', nameLa: 'Sanctus Petrus Martyr, Martyr', rank: 'semiduplex' },
]); ]);
+21 -9
View File
@@ -34,8 +34,12 @@ describe('getDayLabel — ordinal temporal label', () => {
expect(getDayLabel(resolveDay('2026-05-31')).en).toBe( expect(getDayLabel(resolveDay('2026-05-31')).en).toBe(
'The Queenship of the Blessed Virgin Mary (Duplex II Class) — Trinity Sunday', 'The Queenship of the Blessed Virgin Mary (Duplex II Class) — Trinity Sunday',
); );
expect(getDayLabel(resolveDay('2026-04-05')).en).toBe('Easter (Duplex I Class)'); expect(getDayLabel(resolveDay('2026-04-05')).en).toBe(
expect(getDayLabel(resolveDay('2026-02-18')).en).toBe('Ash Wednesday'); "Easter (Duplex I Class) — St. Vincent Ferrer, Confessor (transferred away)",
);
expect(getDayLabel(resolveDay('2026-02-18')).en).toBe(
"Ash Wednesday — St. Simeon, Bishop and Martyr (transferred away)",
);
// Latin: the winner now has a real authored nameLa (see // Latin: the winner now has a real authored nameLa (see
// calendar/data/calendar/saints/queenship-of-the-bvm.yml), so this is // calendar/data/calendar/saints/queenship-of-the-bvm.yml), so this is
// real Latin throughout — the rank label ("Duplex II. Classis") and // real Latin throughout — the rank label ("Duplex II. Classis") and
@@ -44,8 +48,12 @@ describe('getDayLabel — ordinal temporal label', () => {
expect(getDayLabel(resolveDay('2026-05-31')).la).toBe( expect(getDayLabel(resolveDay('2026-05-31')).la).toBe(
'Beata Maria Virgo Regina (Duplex II. Classis) — Dominica Sanctissimae Trinitatis', 'Beata Maria Virgo Regina (Duplex II. Classis) — Dominica Sanctissimae Trinitatis',
); );
expect(getDayLabel(resolveDay('2026-04-05')).la).toBe('Pascha (Duplex I. Classis)'); expect(getDayLabel(resolveDay('2026-04-05')).la).toBe(
expect(getDayLabel(resolveDay('2026-02-18')).la).toBe('Feria Quarta Cinerum'); 'Pascha (Duplex I. Classis) — Sanctus Vincentius Ferrerius, Confessor (translata alio)',
);
expect(getDayLabel(resolveDay('2026-02-18')).la).toBe(
'Feria Quarta Cinerum — Sanctus Simeon, Episcopus et Martyr (translata alio)',
);
}); });
it('gives the correct week-after-Trinity ordinal', () => { it('gives the correct week-after-Trinity ordinal', () => {
@@ -117,7 +125,9 @@ describe('getDayLabel — resumed post-Epiphany Sunday (overflow years)', () =>
expect(getDayLabel(resolveDay('2024-11-11')).en).toBe( expect(getDayLabel(resolveDay('2024-11-11')).en).toBe(
'St. Martin of Tours, Bishop and Confessor (Duplex Majus) — St. Menna, Martyr', 'St. Martin of Tours, Bishop and Confessor (Duplex Majus) — St. Menna, Martyr',
); );
expect(getDayLabel(resolveDay('2024-11-17')).en).toBe('The 6th Sunday after Epiphany (Semiduplex)'); expect(getDayLabel(resolveDay('2024-11-17')).en).toBe(
"The 6th Sunday after Epiphany (Semiduplex) — St. Gregory Thaumaturgus, Bishop and Confessor (transferred away)",
);
// 2024-11-18 is the Dedication of the Basilicas of Ss. Peter and Paul // 2024-11-18 is the Dedication of the Basilicas of Ss. Peter and Paul
// (pre-existing fixed content) -- again a real winning saint, not the // (pre-existing fixed content) -- again a real winning saint, not the
// ferial fallback -- with St. Gregory Thaumaturgus (Semiduplex, // ferial fallback -- with St. Gregory Thaumaturgus (Semiduplex,
@@ -126,7 +136,7 @@ describe('getDayLabel — resumed post-Epiphany Sunday (overflow years)', () =>
// transfers to the next open day rather than being commemorated in // transfers to the next open day rather than being commemorated in
// place, and Nov 18 is that day. // place, and Nov 18 is that day.
expect(getDayLabel(resolveDay('2024-11-18')).en).toBe( expect(getDayLabel(resolveDay('2024-11-18')).en).toBe(
'Dedication of the Basilicas of Ss. Peter and Paul (Duplex) — St. Gregory Thaumaturgus, Bishop and Confessor', 'Dedication of the Basilicas of Ss. Peter and Paul (Duplex) — St. Gregory Thaumaturgus, Bishop and Confessor (transferred)',
); );
// 2035-10-29, a Monday in the 3rd week of a different overflow // 2035-10-29, a Monday in the 3rd week of a different overflow
// stretch, is genuinely clean (no sanctoral entry, no commemoration) // stretch, is genuinely clean (no sanctoral entry, no commemoration)
@@ -147,9 +157,11 @@ describe('getDayLabel — resumed post-Epiphany Sunday (overflow years)', () =>
// Thaumaturgus above -- landing on 2026-11-23 instead, where St. // Thaumaturgus above -- landing on 2026-11-23 instead, where St.
// Clement already natively wins; Cecilia loses that tied collision // Clement already natively wins; Cecilia loses that tied collision
// (both Semiduplex) and joins St. Felicitas as a commemoration there. // (both Semiduplex) and joins St. Felicitas as a commemoration there.
expect(getDayLabel(resolveDay('2026-11-22')).en).toBe('The 23rd Sunday after Trinity (Semiduplex)'); expect(getDayLabel(resolveDay('2026-11-22')).en).toBe(
"The 23rd Sunday after Trinity (Semiduplex) — St. Cecilia, Virgin and Martyr (transferred away)",
);
expect(getDayLabel(resolveDay('2026-11-23')).en).toBe( expect(getDayLabel(resolveDay('2026-11-23')).en).toBe(
'St. Clement I, Pope and Martyr (Semiduplex) — St. Felicitas, Martyr — St. Cecilia, Virgin and Martyr', 'St. Clement I, Pope and Martyr (Semiduplex) — St. Felicitas, Martyr — St. Cecilia, Virgin and Martyr (transferred)',
); );
// 1943-11-21 is also the Presentation of the BVM (Duplex Majus, added // 1943-11-21 is also the Presentation of the BVM (Duplex Majus, added
// 2026-08 — see presentation-of-the-bvm.yml). It's `ordinary-sunday` // 2026-08 — see presentation-of-the-bvm.yml). It's `ordinary-sunday`
@@ -399,7 +411,7 @@ describe('getDayLabel/getVespersStatusLabel — Vespers evening with a temporal
expect(day.date).toBe('2026-08-30'); expect(day.date).toBe('2026-08-30');
expect(day.winner).toEqual({ kind: 'temporal', id: 'post-pentecost-14' }); expect(day.winner).toEqual({ kind: 'temporal', id: 'post-pentecost-14' });
expect(getDayLabel(day).en).toBe( expect(getDayLabel(day).en).toBe(
'The 13th Sunday after Trinity (Semiduplex) — Ss. Felix and Adauctus, Martyrs — The Beheading of St. John the Baptist (of today)', "The 13th Sunday after Trinity (Semiduplex) — Ss. Felix and Adauctus, Martyrs — The Beheading of St. John the Baptist (of today) — St. Rose of Lima, Virgin (transferred away)",
); );
expect(getVespersStatusLabel(day, '2026-08-29').en).toBe('First Vespers (of tomorrow)'); expect(getVespersStatusLabel(day, '2026-08-29').en).toBe('First Vespers (of tomorrow)');
}); });
@@ -85,6 +85,7 @@ describe('November sanctoral pull (first pass)', () => {
name: 'St. Clement I, Pope and Martyr', name: 'St. Clement I, Pope and Martyr',
nameLa: 'Sanctus Clemens I, Papa et Martyr', nameLa: 'Sanctus Clemens I, Papa et Martyr',
rank: 'semiduplex', rank: 'semiduplex',
transferredFrom: '2025-11-23',
}); });
expect(followingDay.commemorations).toEqual([ expect(followingDay.commemorations).toEqual([
{ kind: 'sanctoral', id: 'st-chrysogonus', name: 'St. Chrysogonus, Martyr', nameLa: 'Sanctus Chrysogonus, Martyr', rank: 'simplex' }, { kind: 'sanctoral', id: 'st-chrysogonus', name: 'St. Chrysogonus, Martyr', nameLa: 'Sanctus Chrysogonus, Martyr', rank: 'simplex' },
@@ -28,6 +28,7 @@ describe('September sanctoral pull (first pass)', () => {
name: 'St. Raymond Nonnatus, Confessor', name: 'St. Raymond Nonnatus, Confessor',
nameLa: 'Sanctus Raymundus Nonnatus, Confessor', nameLa: 'Sanctus Raymundus Nonnatus, Confessor',
rank: 'semiduplex', rank: 'semiduplex',
transferredFrom: '2025-08-31',
}); });
expect(day.commemorations).toEqual([ expect(day.commemorations).toEqual([
{ kind: 'sanctoral', id: 'ss-twelve-brothers', name: 'Ss. Twelve Brothers, Martyrs', nameLa: 'Duodecim Fratres, Martyres', rank: 'simplex' }, { kind: 'sanctoral', id: 'ss-twelve-brothers', name: 'Ss. Twelve Brothers, Martyrs', nameLa: 'Duodecim Fratres, Martyres', rank: 'simplex' },
+8 -1
View File
@@ -28,7 +28,14 @@ describe('transfer mechanism (resolveDay integration)', () => {
rank: 'semiduplex', rank: 'semiduplex',
}); });
expect(saturday.commemorations).toEqual([ expect(saturday.commemorations).toEqual([
{ kind: 'sanctoral', id: 'vigil-of-st-lawrence', name: 'Vigil of St. Lawrence', nameLa: 'Vigilia Sancti Laurentii', rank: 'vigil' }, {
kind: 'sanctoral',
id: 'vigil-of-st-lawrence',
name: 'Vigil of St. Lawrence',
nameLa: 'Vigilia Sancti Laurentii',
rank: 'vigil',
transferredFrom: '2026-08-09',
},
]); ]);
// The Sunday's own winner is unaffected by the vigil, but St. Romanus // The Sunday's own winner is unaffected by the vigil, but St. Romanus