Collapse consecutive bible-plan chapters into a citation range
Deploy / deploy (push) Successful in 1m51s

Multi-chapter readings (e.g. Job 34-37, authored as one YAML entry per
chapter) were rendering as "Job 34; Job 35; Job 36; Job 37" instead of
the compact "Job 34-37". Verse-qualified and non-consecutive citations
are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
2026-09-03 11:46:34 -04:00
parent d660f9e531
commit 64a5634557
3 changed files with 54 additions and 4 deletions
+32 -1
View File
@@ -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 {
+3 -3
View File
@@ -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');
});
+19
View File
@@ -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');
});
});