hours: split Saturday's Deuteronomy canticle back into its own two parts
Deploy / deploy (push) Successful in 48s
Deploy / deploy (push) Successful in 48s
Per direct instruction: RB 13 says this canticle "in duas glorias" -- two pieces, each closing with its own Gloria Patri, framed by one shared antiphon (opening before the first, full only after the second) -- not joined into one continuous block the way this was first built. canticleText() now takes an optional verse slice so Saturday can ask for two. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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." }
|
antiphon: { la: "In veritáte tua * exáudi me, Dómine.", en: "In thy truth, O Lord, * listen unto me." }
|
||||||
canticle:
|
canticle:
|
||||||
id: canticum-moysis-deuteronomii
|
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." }
|
antiphon: { la: "Date magnitúdinem * Deo nostro.", en: "Ascribe greatness * to our God." }
|
||||||
laudate:
|
laudate:
|
||||||
antiphon: { la: "In cýmbalis * benesonántibus laudáte Deum.", en: "Praise God * upon the resounding cymbals." }
|
antiphon: { la: "In cýmbalis * benesonántibus laudáte Deum.", en: "Praise God * upon the resounding cymbals." }
|
||||||
|
|||||||
+44
-17
@@ -20,7 +20,11 @@ interface LaudsGroup {
|
|||||||
}
|
}
|
||||||
interface LaudsWeekdayAntiphons {
|
interface LaudsWeekdayAntiphons {
|
||||||
groups: LaudsGroup[];
|
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 };
|
laudate: { antiphon: BilingualText };
|
||||||
}
|
}
|
||||||
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, LaudsWeekdayAntiphons>;
|
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, LaudsWeekdayAntiphons>;
|
||||||
@@ -51,28 +55,32 @@ 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;
|
||||||
|
|
||||||
/** Joins a canticle's verse array into one flowing text block, the same
|
const GLORIA_PATRI = {
|
||||||
* 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 {
|
|
||||||
const canticle = getCanticle(canticleId);
|
|
||||||
const text: Partial<Record<string, string>> = {};
|
|
||||||
const status: Partial<Record<string, 'verified' | 'draft' | 'missing'>> = {};
|
|
||||||
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.',
|
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.',
|
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 (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<Record<string, string>> = {};
|
||||||
|
const status: Partial<Record<string, 'verified' | 'draft' | 'missing'>> = {};
|
||||||
for (const lang of ['la', 'en'] as const) {
|
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) {
|
if (lines.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
text[lang] = `${lines.join('\n')}\n${GLORIA[lang]}`;
|
text[lang] = `${lines.join('\n')}\n${GLORIA_PATRI[lang]}`;
|
||||||
let worst: 'verified' | 'draft' | 'missing' = 'verified';
|
let worst: 'verified' | 'draft' | 'missing' = 'verified';
|
||||||
for (const v of canticle.verses) {
|
for (const v of verses) {
|
||||||
const s = v.status[lang] ?? 'missing';
|
const s = v.status[lang] ?? 'missing';
|
||||||
if (STATUS_RANK[s] > STATUS_RANK[worst]) {
|
if (STATUS_RANK[s] > STATUS_RANK[worst]) {
|
||||||
worst = s;
|
worst = s;
|
||||||
@@ -102,13 +110,32 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
parts.push(...psalmParts(group.psalms, group.antiphon, opening(group.antiphon)));
|
parts.push(...psalmParts(group.psalms, group.antiphon, opening(group.antiphon)));
|
||||||
}
|
}
|
||||||
const canticle = getCanticle(wd.canticle.id);
|
const canticle = getCanticle(wd.canticle.id);
|
||||||
|
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({
|
parts.push({
|
||||||
kind: 'canticle',
|
kind: 'canticle',
|
||||||
canticleId: canticle.id,
|
canticleId: canticle.id,
|
||||||
antiphon: opening(wd.canticle.antiphon),
|
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),
|
text: canticleText(canticle.id),
|
||||||
});
|
});
|
||||||
parts.push({ kind: 'antiphon', text: splitNamedAntiphon(wd.canticle.antiphon).full });
|
}
|
||||||
|
parts.push({ kind: 'antiphon', text: canticleClosing });
|
||||||
parts.push(...psalmParts([148, 149, 150], wd.laudate.antiphon, opening(wd.laudate.antiphon)));
|
parts.push(...psalmParts([148, 149, 150], wd.laudate.antiphon, opening(wd.laudate.antiphon)));
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ describe('resolveOrdo("lauds", ...)', () => {
|
|||||||
expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-trium-puerorum');
|
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
|
// Any Saturday works here: lauds-psalmody doesn't yet model the
|
||||||
// Commune/feast override real practice would apply on a Marian-
|
// Commune/feast override real practice would apply on a Marian-
|
||||||
// Sabbato Saturday (see hours/types.ts's 'lauds-psalmody' doc comment)
|
// 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));
|
.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||||
expect(psalmNumbers).toEqual([66, 50, 142, 148, 149, 150]);
|
expect(psalmNumbers).toEqual([66, 50, 142, 148, 149, 150]);
|
||||||
|
|
||||||
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus');
|
const canticles = ordo.parts.filter((p) => p.kind === 'canticle' && p.canticleId === 'canticum-moysis-deuteronomii');
|
||||||
expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-moysis-deuteronomii');
|
expect(canticles.length).toBe(2);
|
||||||
expect(canticle?.kind === 'canticle' ? canticle.text.text.la : undefined).toContain('Audíte, cæli');
|
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', () => {
|
it('gives the Laudate psalms (148-150) one shared antiphon, opening before 148 and repeated in full after 150', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user