diff --git a/src/propers/bible-plan.ts b/src/propers/bible-plan.ts index 7898153..e6c261b 100644 --- a/src/propers/bible-plan.ts +++ b/src/propers/bible-plan.ts @@ -66,6 +66,37 @@ function citationLabel(citation: ScriptureCitation): string { return citation.verses ? `${book} ${citation.chapter}:${citation.verses}` : `${book} ${citation.chapter}`; } +/** Collapses consecutive same-book, whole-chapter passages (e.g. Job 34, 35, + * 36, 37 — the source data authors one entry per chapter) into a compact + * "Book start-end" range, since `citationLabel` alone would otherwise + * produce a separate "Book N" for each chapter. Verse-qualified citations + * never merge into a range. */ +function formatCitationLabel(passages: ScriptureCitation[]): string { + const segments: string[] = []; + let i = 0; + while (i < passages.length) { + const start = passages[i]!; + if (start.verses) { + segments.push(citationLabel(start)); + i++; + continue; + } + let j = i; + while ( + j + 1 < passages.length && + passages[j + 1]!.book === start.book && + !passages[j + 1]!.verses && + passages[j + 1]!.chapter === passages[j]!.chapter + 1 + ) { + j++; + } + const book = start.book.charAt(0).toUpperCase() + start.book.slice(1); + segments.push(j > i ? `${book} ${start.chapter}-${passages[j]!.chapter}` : `${book} ${start.chapter}`); + i = j + 1; + } + return segments.join('; '); +} + function isGospelReading(passages: ScriptureCitation[]): boolean { return passages.every((p) => GOSPEL_BOOKS.has(p.book)); } @@ -76,7 +107,7 @@ function resolveReading(record: BiblePlanReadingRecord, bookIndex: number): Bibl la: text.la ? 'verified' : 'missing', en: text.en ? 'verified' : 'missing', }; - const label = record.passages.map(citationLabel).join('; '); + const label = formatCitationLabel(record.passages); const firstBook = record.passages[0]?.book; const responsory = firstBook ? getResponsoryForBook(firstBook, bookIndex)?.text : undefined; return { diff --git a/tests/hours/matins.test.ts b/tests/hours/matins.test.ts index 138c6ea..3150d65 100644 --- a/tests/hours/matins.test.ts +++ b/tests/hours/matins.test.ts @@ -85,7 +85,7 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => { // commemorated saint here) — see the next test for the 3-reading case. expect(lessons).toHaveLength(2); const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en); - expect(citations).toEqual(['Isa 6; Isa 7', 'Sap 2']); + expect(citations).toEqual(['Isa 6-7', 'Sap 2']); // The Vulgate/Douay-Rheims bulk import (2026-09-03) landed the real // text for these passages. expect((lessons[0] as { text: { status: { en?: string } } }).text.status.en).toBe('verified'); @@ -105,7 +105,7 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => { const lessons = commemoratedOrdo.parts.filter((p) => p.kind === 'lesson' && !p.label); expect(lessons).toHaveLength(3); const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en); - expect(citations).toEqual(['Isa 6; Isa 7', 'Sap 2', undefined]); + expect(citations).toEqual(['Isa 6-7', 'Sap 2', undefined]); expect((lessons[0] as { text: { status: { en?: string } } }).text.status.en).toBe('verified'); }); @@ -154,7 +154,7 @@ describe('resolveOrdo("matins", ...) Sunday (3-nocturn) branch', () => { it("resolves the user's own bible-plan reading (Tobit/Sirach) for Nocturn 1", () => { const lessons = ordo.parts.filter((p) => p.kind === 'lesson'); const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en); - expect(citations).toContain('Tob 1; Tob 2'); + expect(citations).toContain('Tob 1-2'); expect(citations).toContain('Sir 45'); }); diff --git a/tests/propers/bible-plan.test.ts b/tests/propers/bible-plan.test.ts new file mode 100644 index 0000000..6bb61ec --- /dev/null +++ b/tests/propers/bible-plan.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { getBiblePlanReadings } from '../../src/propers/bible-plan'; + +describe('getBiblePlanReadings citation labels', () => { + const readings = getBiblePlanReadings('post-pentecost-14', 'thursday', '2026-09-17'); + + it('collapses a consecutive multi-chapter run of the same book into a range', () => { + expect(readings[0]?.citation.la).toBe('Job 34-37'); + expect(readings[0]?.citation.en).toBe('Job 34-37'); + }); + + it('leaves a single-chapter reading unchanged', () => { + expect(readings[1]?.citation.la).toBe('Sir 42'); + }); + + it('leaves a verse-qualified citation unchanged, never merged into a range', () => { + expect(readings[2]?.citation.la).toBe('Luke 18:1-23'); + }); +});