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
This commit is contained in:
+58
-11
@@ -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]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user