1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
package bible
import (
"regexp"
"strconv"
"strings"
)
var refRe = regexp.MustCompile(`^(.*?)\s+(\d+):(.+)$`)
// crossChapRangeRe matches a verse group whose range crosses a chapter
// boundary: "M-P:Q" -- from verse M of the chapter it opens in, through verse
// Q of chapter P (e.g. "30-28:7" inside "Sirach 27:30-28:7"). Capture groups:
// 1=M (from-verse), 2=P (to-chapter), 3=Q (to-verse).
var crossChapRangeRe = regexp.MustCompile(`^(\d+)-(\d+):(\d+)$`)
// noUpperBound stands in for "through the end of the chapter" when filtering
// verses in Lookup. It is only ever used to bound a loop over verses that
// already exist in the corpus -- it never appears in a ref string, so it
// cannot leak into SplitRef's output or FormatRef's display.
const noUpperBound = 1<<31 - 1
// SplitRef splits a ref whose verse list mixes single verses and ranges into
// one ref per group (the kjv tools reject a mixed list in a single query). A
// group may itself be a cross-chapter range ("M-P:Q"); it is kept as one
// group (Lookup expands it), but it also updates the chapter that later
// bare-verse groups in the list belong to (e.g. "Malachi 1:14-2:2,8-10" ->
// ["Malachi 1:14-2:2", "Malachi 2:8-10"]).
func SplitRef(ref string) []string {
m := refRe.FindStringSubmatch(ref)
if m == nil {
return []string{ref}
}
book, chap, verses := m[1], m[2], m[3]
if !strings.Contains(verses, ",") {
return []string{ref}
}
var out []string
cur := chap // chapter the next bare (no ":") group belongs to
for _, g := range strings.Split(verses, ",") {
g = strings.TrimSpace(g)
if g == "" {
continue
}
if cm := crossChapRangeRe.FindStringSubmatch(g); cm != nil {
out = append(out, book+" "+cur+":"+g)
cur = cm[2] // groups after this one belong to chapter P
continue
}
if strings.Contains(g, ":") {
out = append(out, book+" "+g)
cur = g[:strings.IndexByte(g, ':')]
continue
}
out = append(out, book+" "+cur+":"+g)
}
return out
}
// Lookup resolves an English-style reference against a version, returning the
// matched verses (in order) and the sub-refs the corpus had no entry for.
func Lookup(version, ref string) ([]Verse, []string) {
var verses []Verse
var missing []string
for _, part := range SplitRef(ref) {
m := refRe.FindStringSubmatch(part)
if m == nil {
missing = append(missing, part)
continue
}
book, ok := ResolveBook(m[1])
if !ok {
missing = append(missing, part)
continue
}
chap, _ := strconv.Atoi(m[2])
found := false
if cm := crossChapRangeRe.FindStringSubmatch(m[3]); cm != nil {
// "M-P:Q": chapter `chap` from verse M to its end, any whole
// chapters in between, then chapter P from verse 1 through Q.
from, _ := strconv.Atoi(cm[1])
toChap, _ := strconv.Atoi(cm[2])
toVerse, _ := strconv.Atoi(cm[3])
for c := chap; c <= toChap; c++ {
lo, hi := 1, noUpperBound
if c == chap {
lo = from
}
if c == toChap {
hi = toVerse
}
for _, v := range Verses(version, book, c) {
if v.Verse >= lo && v.Verse <= hi {
verses = append(verses, v)
found = true
}
}
}
} else {
from, to := verseRange(m[3])
for _, v := range Verses(version, book, chap) {
if v.Verse >= from && v.Verse <= to {
verses = append(verses, v)
found = true
}
}
}
if !found {
missing = append(missing, part)
}
}
return verses, missing
}
// KeyedVerse is a resolved verse tagged with its canonical book, for tools that
// need the (book, chapter, verse) key rather than the text -- e.g. extracting
// the exact set of pericopes a lectionary cites (cmd/clectio-gen).
type KeyedVerse struct {
Book string
Chapter, Verse int
}
// LookupKeyed resolves a reference like Lookup but returns each matched verse's
// (book, chapter, verse) key instead of its text, in the same order and with the
// same splitting/cross-chapter/versification handling -- so the keys correspond
// exactly to the verses Lookup would return for the same version and ref.
func LookupKeyed(version, ref string) []KeyedVerse {
var out []KeyedVerse
for _, part := range SplitRef(ref) {
m := refRe.FindStringSubmatch(part)
if m == nil {
continue
}
book, ok := ResolveBook(m[1])
if !ok {
continue
}
chap, _ := strconv.Atoi(m[2])
if cm := crossChapRangeRe.FindStringSubmatch(m[3]); cm != nil {
from, _ := strconv.Atoi(cm[1])
toChap, _ := strconv.Atoi(cm[2])
toVerse, _ := strconv.Atoi(cm[3])
for c := chap; c <= toChap; c++ {
lo, hi := 1, noUpperBound
if c == chap {
lo = from
}
if c == toChap {
hi = toVerse
}
for _, v := range Verses(version, book, c) {
if v.Verse >= lo && v.Verse <= hi {
out = append(out, KeyedVerse{book, v.Chapter, v.Verse})
}
}
}
} else {
from, to := verseRange(m[3])
for _, v := range Verses(version, book, chap) {
if v.Verse >= from && v.Verse <= to {
out = append(out, KeyedVerse{book, v.Chapter, v.Verse})
}
}
}
}
return out
}
func verseRange(s string) (int, int) {
s = strings.TrimSpace(s)
if i := strings.IndexAny(s, "-–—"); i >= 0 {
from, _ := strconv.Atoi(strings.TrimSpace(s[:i]))
to, _ := strconv.Atoi(strings.TrimSpace(s[i+1:]))
return from, to
}
n, _ := strconv.Atoi(s)
return n, n
}
|