aboutsummaryrefslogtreecommitdiff
path: root/internal/bible/crosschapter_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 23:54:29 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-27 23:54:29 +0200
commit2f20d41921a4ed08d0a7b16246215e0eee5ac512 (patch)
treee09dc26734f16a2285ee3436dcadbe5ccbaf3de9 /internal/bible/crosschapter_test.go
parente6eba32facf520d202f7941f43206e0f95141452 (diff)
downloadlectio-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.go158
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)
+ }
+}