Collapse consecutive bible-plan chapters into a citation range
Deploy / deploy (push) Successful in 1m51s
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:
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user