Let a temporal winner's ferial branch use its own named antiphons
Ferial-side counterpart to the Sunday-branch temporal-override mechanism added earlier today: hours/matins.ts's ferialAntiphonedNocturn now checks a new matins-ferial-named-antiphon-overrides.yml store first, before falling back to the plain per-weekday default. An override's antiphon list is distributed evenly across the weekday's own already-fixed psalm-group count rather than tied to specific psalm numbers, since one entry is meant to be reused across several different weekdays (each with a different psalm set) instead of authored per-day. Same optional weekdays field as the Sunday-side override restricts which days of a multi-day temporalId span actually share the content. General-purpose, not scoped to any one feast — content added next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
This commit is contained in:
+53
-2
@@ -99,6 +99,7 @@ import matinsSundayAdventOverridesData from '../data/hours/matins-sunday-advent-
|
|||||||
import matinsSundayPaschaltideOverridesData from '../data/hours/matins-sunday-paschaltide-nocturn-overrides.yml';
|
import matinsSundayPaschaltideOverridesData from '../data/hours/matins-sunday-paschaltide-nocturn-overrides.yml';
|
||||||
import matinsSundayNamedOverridesData from '../data/hours/matins-sunday-named-nocturn-overrides.yml';
|
import matinsSundayNamedOverridesData from '../data/hours/matins-sunday-named-nocturn-overrides.yml';
|
||||||
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
|
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
|
||||||
|
import matinsFerialNamedOverridesData from '../data/hours/matins-ferial-named-antiphon-overrides.yml';
|
||||||
import type { Weekday } from '../calendar/types';
|
import type { Weekday } from '../calendar/types';
|
||||||
|
|
||||||
type BilingualText = Partial<Record<string, string>>;
|
type BilingualText = Partial<Record<string, string>>;
|
||||||
@@ -227,6 +228,18 @@ interface FerialNocturn {
|
|||||||
type MatinsFerialAntiphons = Record<Exclude<Weekday, 'sunday'>, FerialNocturn>;
|
type MatinsFerialAntiphons = Record<Exclude<Weekday, 'sunday'>, FerialNocturn>;
|
||||||
const ferialAntiphons = matinsFerialAntiphonsData as unknown as MatinsFerialAntiphons;
|
const ferialAntiphons = matinsFerialAntiphonsData as unknown as MatinsFerialAntiphons;
|
||||||
|
|
||||||
|
// Ferial-side counterpart to `MatinsSundayNamedOverride` — a temporal id
|
||||||
|
// whose own real antiphons should replace the plain per-weekday default
|
||||||
|
// in the 1-nocturn branch, reused as-is across however many weekdays
|
||||||
|
// `weekdays` names (own psalm numbers per weekday unchanged, only the
|
||||||
|
// antiphon layer swaps) rather than authored per-weekday — see
|
||||||
|
// matins-ferial-named-antiphon-overrides.yml's own header.
|
||||||
|
interface FerialNamedOverride {
|
||||||
|
weekdays: Weekday[];
|
||||||
|
antiphons: BilingualText[];
|
||||||
|
}
|
||||||
|
const ferialNamedOverrides = matinsFerialNamedOverridesData as unknown as Record<string, FerialNamedOverride>;
|
||||||
|
|
||||||
const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const;
|
const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const;
|
||||||
|
|
||||||
/** Combines a versicle's V. and R. lines into one rendered block — every
|
/** Combines a versicle's V. and R. lines into one rendered block — every
|
||||||
@@ -443,7 +456,12 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
|
|||||||
return resolveCommon('matins-hymn-ferial');
|
return resolveCommon('matins-hymn-ferial');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The plain ferial weekday nocturn's real antiphoned psalmody (see
|
/** The plain ferial weekday nocturn's real antiphoned psalmody — a
|
||||||
|
* temporal winner with a matching `ferialNamedOverrides` entry (2026-09-02,
|
||||||
|
* e.g. the Easter Octave's Wed-Sat) takes precedence and swaps in that
|
||||||
|
* override's own antiphons via `ferialNamedAntiphonedNocturn` instead (see
|
||||||
|
* data/hours/matins-ferial-named-antiphon-overrides.yml's own header);
|
||||||
|
* otherwise the plain per-weekday default (see
|
||||||
* data/hours/matins-ferial-antiphons.yml's own header for how each
|
* data/hours/matins-ferial-antiphons.yml's own header for how each
|
||||||
* weekday's groups were reconciled against psalter-distribution.yml's own
|
* weekday's groups were reconciled against psalter-distribution.yml's own
|
||||||
* "editorial redistribution" of the historical per-weekday psalm set).
|
* "editorial redistribution" of the historical per-weekday psalm set).
|
||||||
@@ -454,8 +472,12 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
|
|||||||
* real single-nocturn ferial office runs straight from the last psalm to
|
* real single-nocturn ferial office runs straight from the last psalm to
|
||||||
* the Capitulum (fixed 2026-09-02, see the data file's own header). */
|
* the Capitulum (fixed 2026-09-02, see the data file's own header). */
|
||||||
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
||||||
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
|
|
||||||
const winner = resolveOfficeWinner(day);
|
const winner = resolveOfficeWinner(day);
|
||||||
|
const namedOverride = winner.kind === 'temporal' ? ferialNamedOverrides[winner.id] : undefined;
|
||||||
|
if (namedOverride && namedOverride.weekdays.includes(day.weekday)) {
|
||||||
|
return ferialNamedAntiphonedNocturn(day, namedOverride, winner);
|
||||||
|
}
|
||||||
|
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
|
||||||
const parts: ResolvedPart[] = [];
|
const parts: ResolvedPart[] = [];
|
||||||
for (const group of nocturn.groups) {
|
for (const group of nocturn.groups) {
|
||||||
const antiphonText = verifiedText(group.antiphon);
|
const antiphonText = verifiedText(group.antiphon);
|
||||||
@@ -476,6 +498,35 @@ function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A ferial named override's antiphons, distributed as evenly as possible
|
||||||
|
* across the weekday's own already-fixed psalm-group count (see
|
||||||
|
* matins-ferial-named-antiphon-overrides.yml's own header) — the psalm
|
||||||
|
* numbers themselves are unchanged from the plain per-weekday default,
|
||||||
|
* only the antiphon layer swaps. */
|
||||||
|
function ferialNamedAntiphonedNocturn(day: LiturgicalDay, override: FerialNamedOverride, winner: DayWinner): ResolvedPart[] {
|
||||||
|
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
|
||||||
|
const chunkSize = Math.ceil(nocturn.groups.length / override.antiphons.length);
|
||||||
|
const parts: ResolvedPart[] = [];
|
||||||
|
override.antiphons.forEach((antiphon, i) => {
|
||||||
|
const chunk = nocturn.groups.slice(i * chunkSize, (i + 1) * chunkSize);
|
||||||
|
if (chunk.length === 0) return;
|
||||||
|
const antiphonText = verifiedText(antiphon);
|
||||||
|
const opening = openingAntiphon(antiphonText, winner);
|
||||||
|
const { full } = splitNamedAntiphon(antiphonText);
|
||||||
|
let isFirst = true;
|
||||||
|
for (const group of chunk) {
|
||||||
|
for (const ref of group.psalms) {
|
||||||
|
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v): ResolvedVerse => ({ n: v.n, text: v.text, status: v.status }));
|
||||||
|
const { verses, antiphon: partAntiphon } = applyFlexaMark(rawVerses, isFirst ? opening : undefined);
|
||||||
|
isFirst = false;
|
||||||
|
parts.push({ kind: 'psalm', psalmNumber: ref.number, verses, antiphon: partAntiphon });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push({ kind: 'antiphon', text: full });
|
||||||
|
});
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fallback for a Semiduplex+ weekday feast with no matins-psalmody-overrides
|
/** Fallback for a Semiduplex+ weekday feast with no matins-psalmody-overrides
|
||||||
* entry authored for its winner yet: the plain ferial weekday table,
|
* entry authored for its winner yet: the plain ferial weekday table,
|
||||||
* chunked into 3 nocturns instead of 1. Bare (no antiphons, no canticles)
|
* chunked into 3 nocturns instead of 1. Bare (no antiphons, no canticles)
|
||||||
|
|||||||
Reference in New Issue
Block a user