diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 23:54:29 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 23:54:29 +0200 |
| commit | 2f20d41921a4ed08d0a7b16246215e0eee5ac512 (patch) | |
| tree | e09dc26734f16a2285ee3436dcadbe5ccbaf3de9 /internal/bible/crosschapter_test.go | |
| parent | e6eba32facf520d202f7941f43206e0f95141452 (diff) | |
| download | lectio-2f20d41921a4ed08d0a7b16246215e0eee5ac512.tar.gz lectio-2f20d41921a4ed08d0a7b16246215e0eee5ac512.zip | |
feat(bible): resolve and display cross-chapter verse ranges (N:M-P:Q)
Lookup now detects a verse-tail range that crosses a chapter boundary
("M-P:Q", e.g. "30-28:7" in "Sirach 27:30-28:7") and fetches chapter N
from verse M to its end, any whole chapters between, and chapter P from
verse 1 through Q, instead of misreading the tail as a plain from-to
range and coming up empty. SplitRef expands the same pattern inside a
comma list so a trailing group after the jump (e.g. the ",8-10" in
"Malachi 1:14-2:2,8-10") resolves against the new current chapter.
FormatRef renders the range intact as "<sigla> N:M-P:Q" instead of
splitting off a fake new-chapter marker from the embedded "P:" and
mangling the display (previously "Sir 27:7").
No sentinel value is ever stored in a ref string; it only bounds an
internal verse-filter loop in Lookup.
Diffstat (limited to 'internal/bible/crosschapter_test.go')
| -rw-r--r-- | internal/bible/crosschapter_test.go | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/internal/bible/crosschapter_test.go b/internal/bible/crosschapter_test.go new file mode 100644 index 0000000..7fb61eb --- /dev/null +++ b/internal/bible/crosschapter_test.go @@ -0,0 +1,158 @@ +package bible + +import "testing" + +// TestCrossChapterRanges covers the 8 real of-lectionary.ini citations whose +// verse span crosses a chapter boundary ("N:M-P:Q"), e.g. "Sirach 27:30-28:7". +// Each must: parse via ParseRef, resolve a non-empty verse slice spanning +// both chapters via Lookup, and format back to the clean "N:M-P:Q" form via +// FormatRef (no sentinel, no mangled chapter). +func TestCrossChapterRanges(t *testing.T) { + tbl, err := LoadBookTable(nil) + if err != nil { + t.Fatal(err) + } + cases := []struct { + citation string + fromChap, toChap int + wantDisplay string + // spanBoth is false only for the one citation where the corpus's own + // versification (Vulgate/DRB tradition) doesn't have a chapter-8 + // verse 23 at all: Isaiah 8:23 (Hebrew/NAB numbering, used by the + // lectionary) is folded into Isaiah 9:1 in the Vulgate/DRB/Wujek + // tradition -- a real textual/versification difference, not a + // parser bug. The range-crossing fetch itself is exercised + // correctly (it asks for chapter 8 from v23 and finds nothing + // there, then chapter 9 through v3); it just has nothing to find in + // chapter 8 of this corpus. + spanBoth bool + }{ + {"Genesis 1:1-2:2", 1, 2, "Gen 1:1-2:2", true}, + {"Isaiah 52:13-53:12", 52, 53, "Isa 52:13-53:12", true}, + {"Isaiah 8:23-9:3", 8, 9, "Isa 8:23-9:3", false}, + {"John 18:1-19:42", 18, 19, "Jn 18:1-19:42", true}, + {"Malachi 1:14-2:2", 1, 2, "Mal 1:14-2:2", true}, + {"Mat 9:36-10:8", 9, 10, "Matt 9:36-10:8", true}, + {"Sirach 27:30-28:7", 27, 28, "Sir 27:30-28:7", true}, + {"1 John 1:5-2:2", 1, 2, "1 Jn 1:5-2:2", true}, + } + for _, c := range cases { + t.Run(c.citation, func(t *testing.T) { + engRef, ok := tbl.ParseRef("en", c.citation) + if !ok { + t.Fatalf("ParseRef(%q) failed", c.citation) + } + + vs, missing := Lookup("drb", engRef) + if len(vs) == 0 { + t.Fatalf("Lookup(drb, %q) empty; missing=%v", engRef, missing) + } + if len(missing) != 0 { + t.Errorf("Lookup(drb, %q) missing=%v (want none -- some text should resolve)", engRef, missing) + } + last := vs[len(vs)-1] + if last.Chapter != c.toChap { + t.Errorf("last verse chapter = %d want %d (verse %+v)", last.Chapter, c.toChap, last) + } + for _, v := range vs { + if v.Chapter < c.fromChap || v.Chapter > c.toChap { + t.Errorf("verse outside expected chapter range: %+v", v) + } + } + if c.spanBoth { + first := vs[0] + if first.Chapter != c.fromChap { + t.Errorf("first verse chapter = %d want %d (verse %+v)", first.Chapter, c.fromChap, first) + } + sawFrom, sawTo := false, false + for _, v := range vs { + if v.Chapter == c.fromChap { + sawFrom = true + } + if v.Chapter == c.toChap { + sawTo = true + } + } + if !sawFrom || !sawTo { + t.Errorf("range does not span both chapters: sawFrom=%v sawTo=%v verses=%d", sawFrom, sawTo, len(vs)) + } + } + + display := tbl.FormatRef("en", engRef) + if display != c.wantDisplay { + t.Errorf("FormatRef(en, %q) = %q want %q", engRef, display, c.wantDisplay) + } + }) + } +} + +// TestCrossChapterRangeWithTrailingGroup covers the compound real citation +// "Malachi 1:14-2:2,8-10" (Ordinary Sunday 31 A): a cross-chapter range +// followed by a plain verse group in the new (second) chapter. +func TestCrossChapterRangeWithTrailingGroup(t *testing.T) { + tbl, err := LoadBookTable(nil) + if err != nil { + t.Fatal(err) + } + engRef, ok := tbl.ParseRef("en", "Malachi 1:14-2:2,8-10") + if !ok { + t.Fatalf("ParseRef failed") + } + if engRef != "Malachi 1:14-2:2,8-10" { + t.Errorf("ParseRef canonical = %q", engRef) + } + + vs, missing := Lookup("drb", engRef) + if len(vs) == 0 { + t.Fatalf("Lookup(drb, %q) empty; missing=%v", engRef, missing) + } + // Expect: 1:14-19 (end of chap 1), 2:1-2, 2:8-10. + var got []struct{ Chapter, Verse int } + for _, v := range vs { + got = append(got, struct{ Chapter, Verse int }{v.Chapter, v.Verse}) + } + if got[0].Chapter != 1 || got[0].Verse != 14 { + t.Errorf("first verse = %+v want chap 1 verse 14", got[0]) + } + last := got[len(got)-1] + if last.Chapter != 2 || last.Verse != 10 { + t.Errorf("last verse = %+v want chap 2 verse 10", last) + } + sawChap2v2, sawChap2v8 := false, false + for _, g := range got { + if g.Chapter == 2 && g.Verse == 2 { + sawChap2v2 = true + } + if g.Chapter == 2 && g.Verse == 8 { + sawChap2v8 = true + } + } + if !sawChap2v2 || !sawChap2v8 { + t.Errorf("expected verses 2:2 and 2:8 present, got %+v", got) + } + + display := tbl.FormatRef("en", engRef) + if display != "Mal 1:14-2:2,8-10" { + t.Errorf("FormatRef = %q want %q", display, "Mal 1:14-2:2,8-10") + } +} + +// TestSplitRefCrossChapter verifies SplitRef's expansion of a cross-chapter +// group inside a comma list, and that a single cross-chapter range with no +// comma is left as one part (Lookup expands it directly). +func TestSplitRefCrossChapter(t *testing.T) { + if g := SplitRef("Sirach 27:30-28:7"); len(g) != 1 || g[0] != "Sirach 27:30-28:7" { + t.Errorf("SplitRef single cross-chapter = %v", g) + } + got := SplitRef("Malachi 1:14-2:2,8-10") + want := []string{"Malachi 1:14-2:2", "Malachi 2:8-10"} + if len(got) != 2 || got[0] != want[0] || got[1] != want[1] { + t.Errorf("SplitRef cross-chapter + trailing group = %v want %v", got, want) + } + // Existing semicolon-turned-comma precedent must still work. + got2 := SplitRef("Judith 13:22-25,15:10") + want2 := []string{"Judith 13:22-25", "Judith 15:10"} + if len(got2) != 2 || got2[0] != want2[0] || got2[1] != want2[1] { + t.Errorf("SplitRef disjoint cross-chapter = %v want %v", got2, want2) + } +} |
