Give each half of Saturday's split Deuteronomy canticle its own citation
Deploy / deploy (push) Successful in 1m27s
Deploy / deploy (push) Successful in 1m27s
canticleText() always returned the whole canticle's stored citation
("Deut 32:1-65"), even when only slicing out half its verses -- both
Saturday Lauds pieces showed the same full range regardless of which
half was actually being said. Derive the citation from the slice's own
first/last verse numbers instead, only when slicing; a full canticle's
stored citation is left as-is, since it already matches what's shown
and a formula can't reproduce an irregular one like
canticum-trium-puerorum's "Dan 3:57-88,56" (a non-contiguous liturgical
splice).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
This commit is contained in:
+27
-2
@@ -3,7 +3,7 @@ import type { LiturgicalDay, Weekday } from '../calendar/types';
|
|||||||
import { resolveDay } from '../calendar';
|
import { resolveDay } from '../calendar';
|
||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
import { getCanticle } from './lauds-canticles';
|
import { getCanticle, type CanticleVerse } from './lauds-canticles';
|
||||||
import { getLaudsSaintOverride, getLaudsCommonOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
import { getLaudsSaintOverride, getLaudsCommonOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
||||||
import { getOpeningVersicleId } from './opening-versicle';
|
import { getOpeningVersicleId } from './opening-versicle';
|
||||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||||
@@ -145,6 +145,30 @@ function psalmParts(numbers: number[], antiphon: BilingualText, opening: Resolve
|
|||||||
|
|
||||||
const STATUS_RANK = { verified: 0, draft: 1, missing: 2 } as const;
|
const STATUS_RANK = { verified: 0, draft: 1, missing: 2 } as const;
|
||||||
|
|
||||||
|
/** The general rule is that a citation must match whatever verses are
|
||||||
|
* actually being displayed — not "the whole canticle's citation,
|
||||||
|
* always." A full canticle's stored `citation` already satisfies that
|
||||||
|
* (canticleText below only calls this helper for a `slice`), including
|
||||||
|
* cases a formula couldn't reconstruct — canticum-trium-puerorum's
|
||||||
|
* "Dan 3:57-88,56" is a non-contiguous liturgical splice, its last verse
|
||||||
|
* (`n: "3:56"`) coming after verse 88 in display order, not after it
|
||||||
|
* numerically. Only Saturday's split Deuteronomy canticle ever shows
|
||||||
|
* fewer than all of a canticle's verses, so it's the only case needing
|
||||||
|
* this: book name from the canticle's base citation (its first word),
|
||||||
|
* verse range from the slice's own first/last `n` values (e.g.
|
||||||
|
* "32:1"/"32:32" -> "Deut 32:1-32") — showing the full "1-65" range on
|
||||||
|
* both halves would mislabel whichever half isn't actually being said. */
|
||||||
|
function sliceCitation(baseCitation: string, verses: CanticleVerse[]): string {
|
||||||
|
const book = baseCitation.split(' ')[0] ?? baseCitation;
|
||||||
|
const first = verses[0]?.n;
|
||||||
|
const last = verses[verses.length - 1]?.n;
|
||||||
|
if (!first || !last) {
|
||||||
|
return baseCitation;
|
||||||
|
}
|
||||||
|
const lastVerse = last.split(':')[1] ?? last;
|
||||||
|
return `${book} ${first}-${lastVerse}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Joins a canticle's verse array (or a `[start, end)` slice of it — see
|
/** Joins a canticle's verse array (or a `[start, end)` slice of it — see
|
||||||
* Saturday's split canticle below) into one flowing text block, the same
|
* Saturday's split canticle below) into one flowing text block, the same
|
||||||
* shape nunc-dimittis/benedictus already use — Lauds' weekday canticles
|
* shape nunc-dimittis/benedictus already use — Lauds' weekday canticles
|
||||||
@@ -174,7 +198,8 @@ function canticleText(canticleId: string, slice?: [number, number]): ResolvedTex
|
|||||||
}
|
}
|
||||||
status[lang] = worst;
|
status[lang] = worst;
|
||||||
}
|
}
|
||||||
return { text, status, citation: { la: canticle.citation, en: canticle.citation } };
|
const citation = slice ? sliceCitation(canticle.citation, verses) : canticle.citation;
|
||||||
|
return { text, status, citation: { la: citation, en: citation } };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ps 66 through the Laudate psalms — see hours/types.ts's 'lauds-psalmody'
|
/** Ps 66 through the Laudate psalms — see hours/types.ts's 'lauds-psalmody'
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ describe('resolveOrdo("lauds", ...)', () => {
|
|||||||
|
|
||||||
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus');
|
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus');
|
||||||
expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-trium-puerorum');
|
expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-trium-puerorum');
|
||||||
|
// Its non-contiguous liturgical citation (last verse said out of
|
||||||
|
// numeric order) must come through as authored, not get mangled by
|
||||||
|
// a first/last-verse-number formula the way a naive fix would.
|
||||||
|
expect(canticle?.kind === 'canticle' ? canticle.text.citation?.la : undefined).toBe('Dan 3:57-88,56');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves Saturday's own psalmody: only one weekday psalm (142), and the Canticle of Moses said in two pieces (RB 13's own division), one shared antiphon framing both", () => {
|
it("resolves Saturday's own psalmody: only one weekday psalm (142), and the Canticle of Moses said in two pieces (RB 13's own division), one shared antiphon framing both", () => {
|
||||||
@@ -78,6 +82,10 @@ describe('resolveOrdo("lauds", ...)', () => {
|
|||||||
expect(part2?.kind === 'canticle' ? part2.antiphon : 'should be undefined -- shares part 1s opening antiphon').toBeUndefined();
|
expect(part2?.kind === 'canticle' ? part2.antiphon : 'should be undefined -- shares part 1s opening antiphon').toBeUndefined();
|
||||||
expect(part1?.kind === 'canticle' ? part1.text.text.la : undefined).toContain('Audíte, cæli');
|
expect(part1?.kind === 'canticle' ? part1.text.text.la : undefined).toContain('Audíte, cæli');
|
||||||
expect(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Ignis succénsus est');
|
expect(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Ignis succénsus est');
|
||||||
|
// Regression test: each half must cite its own verse range, not the
|
||||||
|
// whole canticle's "Deut 32:1-65" on both pieces.
|
||||||
|
expect(part1?.kind === 'canticle' ? part1.text.citation?.la : undefined).toBe('Deut 32:1-32');
|
||||||
|
expect(part2?.kind === 'canticle' ? part2.text.citation?.la : undefined).toBe('Deut 32:33-65');
|
||||||
// Each part gets its own Gloria Patri.
|
// Each part gets its own Gloria Patri.
|
||||||
expect(part1?.kind === 'canticle' ? part1.gloriaPatri?.text.la : undefined).toContain('Glória Patri');
|
expect(part1?.kind === 'canticle' ? part1.gloriaPatri?.text.la : undefined).toContain('Glória Patri');
|
||||||
expect(part2?.kind === 'canticle' ? part2.gloriaPatri?.text.la : undefined).toContain('Glória Patri');
|
expect(part2?.kind === 'canticle' ? part2.gloriaPatri?.text.la : undefined).toContain('Glória Patri');
|
||||||
|
|||||||
Reference in New Issue
Block a user