diff --git a/src/data/hours/lauds-antiphons.yml b/src/data/hours/lauds-antiphons.yml index b62dfbd..4a9368a 100644 --- a/src/data/hours/lauds-antiphons.yml +++ b/src/data/hours/lauds-antiphons.yml @@ -96,6 +96,14 @@ saturday: antiphon: { la: "In veritáte tua * exáudi me, Dómine.", en: "In thy truth, O Lord, * listen unto me." } canticle: id: canticum-moysis-deuteronomii + # Split across two D.O. Lauds slots (own doc comment on the split, own + # Gloria Patri each -- confirmed live) rather than joined into one + # continuous canticle the way every other weekday's is: per direct + # instruction, this is the older monastic practice, matching how the + # Rule of St. Benedict itself divides it (RB 13's "Canticum de + # Deuteronomio... in duas glorias"). `split: 32` is the verse count of + # the first part (Deut 32:1-32); the rest (32:33-65) is the second. + split: 32 antiphon: { la: "Date magnitúdinem * Deo nostro.", en: "Ascribe greatness * to our God." } laudate: antiphon: { la: "In cýmbalis * benesonántibus laudáte Deum.", en: "Praise God * upon the resounding cymbals." } diff --git a/src/hours/lauds.ts b/src/hours/lauds.ts index 7c79b09..68463db 100644 --- a/src/hours/lauds.ts +++ b/src/hours/lauds.ts @@ -20,7 +20,11 @@ interface LaudsGroup { } interface LaudsWeekdayAntiphons { groups: LaudsGroup[]; - canticle: { id: string; antiphon: BilingualText }; + // `split`: verse count of the canticle's first part, when it's said in + // two pieces (own Gloria Patri each) rather than continuously — see + // lauds-antiphons.yml's saturday.canticle comment. Absent every other + // weekday. + canticle: { id: string; antiphon: BilingualText; split?: number }; laudate: { antiphon: BilingualText }; } const laudsAntiphons = laudsAntiphonsData as Record; @@ -51,28 +55,32 @@ function psalmParts(numbers: number[], antiphon: BilingualText, opening: Resolve const STATUS_RANK = { verified: 0, draft: 1, missing: 2 } as const; -/** Joins a canticle's verse array into one flowing text block, the same +const GLORIA_PATRI = { + la: 'V. Glória Patri, et Fílio, * et Spirítui Sancto.\nR. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen.', + en: 'V. Glory be to the Father, and to the Son, * and to the Holy Ghost.\nR. As it was in the beginning, is now, * and ever shall be, world without end. Amen.', +}; + +/** 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 * shape nunc-dimittis/benedictus already use — Lauds' weekday canticles * are stored verse-by-verse (see lauds-canticles.ts) purely because that's * how they were transcribed, not because callers need per-verse access. - * Appends the fixed Gloria Patri line every canticle ends with in real - * practice, same wording as benedictus.yml's own trailing line. */ -function canticleText(canticleId: string): ResolvedText { + * Appends the fixed Gloria Patri line every canticle (or canticle part) + * ends with in real practice, same wording as benedictus.yml's own + * trailing line. */ +function canticleText(canticleId: string, slice?: [number, number]): ResolvedText { const canticle = getCanticle(canticleId); + const verses = slice ? canticle.verses.slice(slice[0], slice[1]) : canticle.verses; const text: Partial> = {}; const status: Partial> = {}; - const GLORIA = { - la: 'V. Glória Patri, et Fílio, * et Spirítui Sancto.\nR. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen.', - en: 'V. Glory be to the Father, and to the Son, * and to the Holy Ghost.\nR. As it was in the beginning, is now, * and ever shall be, world without end. Amen.', - }; for (const lang of ['la', 'en'] as const) { - const lines = canticle.verses.map((v) => v.text[lang]).filter((t): t is string => Boolean(t)); + const lines = verses.map((v) => v.text[lang]).filter((t): t is string => Boolean(t)); if (lines.length === 0) { continue; } - text[lang] = `${lines.join('\n')}\n${GLORIA[lang]}`; + text[lang] = `${lines.join('\n')}\n${GLORIA_PATRI[lang]}`; let worst: 'verified' | 'draft' | 'missing' = 'verified'; - for (const v of canticle.verses) { + for (const v of verses) { const s = v.status[lang] ?? 'missing'; if (STATUS_RANK[s] > STATUS_RANK[worst]) { worst = s; @@ -102,13 +110,32 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] { parts.push(...psalmParts(group.psalms, group.antiphon, opening(group.antiphon))); } const canticle = getCanticle(wd.canticle.id); - parts.push({ - kind: 'canticle', - canticleId: canticle.id, - antiphon: opening(wd.canticle.antiphon), - text: canticleText(canticle.id), - }); - parts.push({ kind: 'antiphon', text: splitNamedAntiphon(wd.canticle.antiphon).full }); + const canticleOpening = opening(wd.canticle.antiphon); + const canticleClosing = splitNamedAntiphon(wd.canticle.antiphon).full; + if (wd.canticle.split) { + // Said in two pieces, own Gloria Patri each, one shared antiphon + // framing both (opening before the first, full repeated only after + // the second) — see lauds-antiphons.yml's saturday.canticle comment. + parts.push({ + kind: 'canticle', + canticleId: canticle.id, + antiphon: canticleOpening, + text: canticleText(canticle.id, [0, wd.canticle.split]), + }); + parts.push({ + kind: 'canticle', + canticleId: canticle.id, + text: canticleText(canticle.id, [wd.canticle.split, canticle.verses.length]), + }); + } else { + parts.push({ + kind: 'canticle', + canticleId: canticle.id, + antiphon: canticleOpening, + text: canticleText(canticle.id), + }); + } + parts.push({ kind: 'antiphon', text: canticleClosing }); parts.push(...psalmParts([148, 149, 150], wd.laudate.antiphon, opening(wd.laudate.antiphon))); return parts; } diff --git a/tests/hours/lauds.test.ts b/tests/hours/lauds.test.ts index d2d971f..049504d 100644 --- a/tests/hours/lauds.test.ts +++ b/tests/hours/lauds.test.ts @@ -57,7 +57,7 @@ describe('resolveOrdo("lauds", ...)', () => { expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-trium-puerorum'); }); - it("resolves Saturday's own psalmody: only one weekday psalm (142), and the Canticle of Moses split across two D.O. slots joins into one continuous canticle", () => { + 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", () => { // Any Saturday works here: lauds-psalmody doesn't yet model the // Commune/feast override real practice would apply on a Marian- // Sabbato Saturday (see hours/types.ts's 'lauds-psalmody' doc comment) @@ -69,9 +69,21 @@ describe('resolveOrdo("lauds", ...)', () => { .map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); expect(psalmNumbers).toEqual([66, 50, 142, 148, 149, 150]); - const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus'); - expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-moysis-deuteronomii'); - expect(canticle?.kind === 'canticle' ? canticle.text.text.la : undefined).toContain('Audíte, cæli'); + const canticles = ordo.parts.filter((p) => p.kind === 'canticle' && p.canticleId === 'canticum-moysis-deuteronomii'); + expect(canticles.length).toBe(2); + const [part1, part2] = canticles; + expect(part1?.kind === 'canticle' ? part1.antiphon?.text.la : undefined).toBe('Ant. Date magnitúdinem'); + 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(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Ignis succénsus est'); + // Each part gets its own Gloria Patri. + expect(part1?.kind === 'canticle' ? part1.text.text.la : undefined).toContain('Glória Patri'); + expect(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Glória Patri'); + + const repeatIndex = ordo.parts.indexOf(part2!) + 1; + const repeat = ordo.parts[repeatIndex]; + expect(repeat?.kind).toBe('antiphon'); + expect(repeat?.kind === 'antiphon' ? repeat.text.text.la : undefined).toBe('Ant. Date magnitúdinem * Deo nostro.'); }); it('gives the Laudate psalms (148-150) one shared antiphon, opening before 148 and repeated in full after 150', () => {