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 }