Add Dec 26-30's own Sunday rule -- a deliberate remix, not a live-verified fix
Deploy / deploy (push) Successful in 54s

Per direct instruction, and explicitly framed as this project's own
departure from Monastic 1617 itself: within Dec 26-30 inclusive (exactly
one real Sunday falls somewhere in that window every year), whichever
date is the actual Sunday always wins as Nat1-0 outright, with no rank
check at all -- even Stephen/John/Holy Innocents (Duplex II. classis)
get fully displaced, not just commemorated. The displaced saint isn't
commemorated in place either; he reappears in full as Dec 30's own
winner instead (the block's one date with no fixed saint of its own).
Rank never matters for this rule -- confirmed with Becket (Semiduplex)
transferring the same way as the Duplex-II-classis saints.

This deliberately diverges both from what Monastic 1617 itself does on
Dec 26-29 (live-verified two commits ago: the saint wins there, Sunday
merely commemorated) and from the general privileged-sunday transfer
rule elsewhere (which only transfers ranks below duplex-majus and
commemorates everything else in place) -- this window transfers every
rank and commemorates nothing in place, a genuinely new, third
behavior specific to this 5-day block.

Implemented as calendar/index.ts's applyChristmasOctaveSunday, a
narrowly-scoped layer in the same spirit as applyMarianSaturday/
applyChristTheKing, rather than by changing the shared
decideOccurrence/transfer machinery every other Sunday relies on. Dec 31
and everything outside Dec 26-30 is untouched -- St. Silvester still
beats the Sunday there via the plain, unchanged ordinary-sunday rule.

Verified against all five possible Sunday positions within the window
(Dec 26/27/28/29/30) plus the "nothing displaces anything" control case,
each with its own dedicated test in the new
tests/calendar/christmas-octave-sunday.test.ts. Updates two existing
tests whose assertions were correct under the old rule and are now
superseded specifically within this window (Stephen/Innocents no longer
win their own Sunday-collision dates there) while confirming the
underlying ordinary-sunday rule is still live and real outside it (Dec
31/St. Silvester).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 10:59:34 -04:00
parent 397f31f4fc
commit ea29e5ee45
5 changed files with 231 additions and 33 deletions
+60
View File
@@ -103,6 +103,7 @@ export function resolveDay(isoDate: string): LiturgicalDay {
winner = applyOctaves(isoDate, winner, commemorations);
winner = applyMarianSaturday(isoDate, weekday, temporalCategory, winner, commemorations);
winner = applyChristTheKing(isoDate, winner, commemorations);
winner = applyChristmasOctaveSunday(isoDate, weekday, winner, commemorations);
return { date: isoDate, weekday, season, temporalCategory, winner, commemorations };
}
@@ -137,6 +138,65 @@ function applyChristTheKing(isoDate: string, winner: DayWinner, commemorations:
return { kind: 'temporal', id: 'christ-the-king' };
}
/**
* Per direct instruction, scoped to exactly Dec 26-30 — every year
* contains exactly one real Sunday somewhere in that 5-day window.
*
* On whichever of Dec 26-29 it actually falls, Nat1-0 (the Sunday within
* the Octave of the Nativity) always wins outright, full stop — no rank
* check at all, a deliberate departure from what Monastic 1617 itself
* does there (a Duplex-II-classis saint — Stephen/John/Innocents — beats
* the Sunday there in the real engine; see TODO.md). The displaced
* saint isn't even commemorated in place; he reappears in full on Dec
* 30 instead — the block's one date with no fixed saint of its own —
* rather than the general privileged-sunday transfer/commemorate split
* used everywhere else. Dec 30 itself, on a year it's the *actual*
* Sunday (nothing displaced), resolves normally: Nat1-0 wins clean since
* nothing was ever assigned there to begin with.
*/
function applyChristmasOctaveSunday(
isoDate: string,
weekday: ReturnType<typeof weekdayOf>,
winner: DayWinner,
commemorations: Commemoration[],
): DayWinner {
const [year, monthStr, dayStr] = isoDate.split('-');
if (monthStr !== '12') {
return winner;
}
const day = Number(dayStr);
if (day >= 26 && day <= 29 && weekday === 'sunday') {
return { kind: 'temporal', id: 'christmas-octave-sunday' };
}
if (day === 30) {
for (let d = 26; d <= 29; d++) {
const candidateDate = `${year}-12-${String(d).padStart(2, '0')}`;
if (weekdayOf(candidateDate) !== 'sunday') {
continue;
}
const displaced = getSanctoralCandidatesFor(candidateDate)[0];
if (!displaced) {
break;
}
// Drop the now-redundant "Octave of <displaced>" commemoration —
// applyOctaves already pushed one above, using Dec 30's *original*
// temporal winner; now that the displaced saint is winning the day
// outright instead, commemorating his own octave alongside himself
// would be the same redundancy applyOctaves' own `isSelf` check
// already avoids on a saint's actual day.
const idx = commemorations.findIndex((c) => c.kind === 'octave' && c.id === displaced.id);
if (idx !== -1) {
commemorations.splice(idx, 1);
}
return { kind: 'sanctoral', id: displaced.id, name: displaced.name, rank: displaced.rank };
}
}
return winner;
}
/**
* Layered on top of everything above, same spirit as applyOctaves: never
* changes decideOccurrence's own rules, just relabels the result on a free