claude go reimplement

This commit is contained in:
tyler committed 2026-10-26 20:30:43 -07:00
1 parent 4bcb924158
commit f523c79618
83 files changed
+3714 -3454

No files matched your search

+357
View File
@@ -0,0 +1,357 @@
package kjv
// Scripture reference resolver. Understands the many ways people type a
// reference ("1 jn 3.16", "II Kings 2:1-4", "song of songs 2", "Exodus ") and
// turns it into ranges of verse ids (book*1000000 + chapter*1000 + verse).
import (
"regexp"
"sort"
"strconv"
"strings"
)
// Extra spellings people use that the abbreviations table does not know.
var extraBookAliases = map[string][]string{
"Song of Solomon": {"song of songs", "songs of solomon", "songs of songs", "song of sol",
"song of sol", "canticles", "canticle of canticles", "sos", "sng", "ss"},
"Psalms": {"psalm", "psalter", "pslm", "psa", "pss", "psm", "ps"},
"Revelation": {"revelations", "revelation of john", "revelation of st john", "the revelation", "apocalypse"},
"Ecclesiastes": {"ecclesiasts", "eccles", "ecc", "qoheleth"},
"Philemon": {"phlm", "phm", "philem"},
"Judges": {"jdg", "jdgs", "jg"},
"Mark": {"mrk", "mk", "mr"},
"John": {"jhn", "jn"},
"Acts": {"acts of the apostles", "act"},
"James": {"jas", "jm"},
}
// Words that are books but also ordinary search words: a bare "numbers" or
// "mark" still shows the book, but /find offers a keyword search as well.
var bareBookExtra = map[string]bool{
"psalm": true, "songofsongs": true, "songsofsolomon": true, "songofsol": true, "canticles": true,
"canticleofcanticles": true, "revelations": true, "revelationofjohn": true, "therevelation": true,
}
const maxChapterIDSpan = 999
// A reference is never this long; longer text goes to the keyword search. Bounds the fuzzy matching work.
const maxRefQueryLen = 300
// Go's regexp has no lookaheads, so they are written as a consumed [\s.] here, which matches the same text.
var (
bookPieceRE = regexp.MustCompile(`(?i)^\s*(?P<book>(?:(?:[1-3]|(?:iii|ii|i)[\s.]|1st|2nd|3rd|first|second|third)\.?\s*)?[a-z][a-z.\s]*?)` +
`\s*(?P<rest>\d[\d\s:.\-–—]*)?\s*$`)
continuationRE = regexp.MustCompile(`^\s*\d[\d\s:.\-–—]*$`)
reBookOf = regexp.MustCompile(`^(?:the\s+)?(?:book\s+of\s+)`)
reFirst = regexp.MustCompile(`^(?:1st|first)([\s.a-z]|$)`)
reSecond = regexp.MustCompile(`^(?:2nd|second)([\s.a-z]|$)`)
reThird = regexp.MustCompile(`^(?:3rd|third)([\s.a-z]|$)`)
reRoman3 = regexp.MustCompile(`^iii([\s.]|$)`)
reRoman2 = regexp.MustCompile(`^ii([\s.]|$)`)
reRoman1 = regexp.MustCompile(`^i([\s.])`)
reNotAlnum = regexp.MustCompile(`[^a-z0-9]`)
reSepSpace = regexp.MustCompile(`\s*([:\-])\s*`)
reChapter = regexp.MustCompile(`^(\d+)$`)
reChapterAll = regexp.MustCompile(`^(\d+)(?::-|-)$`)
reChapterRange = regexp.MustCompile(`^(\d+)-(\d+)$`)
reVerse = regexp.MustCompile(`^(\d+)[:.\s](\d+)$`)
reVerseOn = regexp.MustCompile(`^(\d+)[:.\s](\d+)-$`)
reVerseRange = regexp.MustCompile(`^(\d+)[:.\s](\d+)-(\d+)$`)
reVerseAcrossCh = regexp.MustCompile(`^(\d+)[:.\s](\d+)-(\d+)[:.](\d+)$`)
)
// normalizeBookText: 'I Cor.' / '1st Corinthians' / '1 corinthians' -> '1corinthians'
func normalizeBookText(text string) string {
text = strings.ToLower(strings.TrimSpace(text))
text = reBookOf.ReplaceAllString(text, "")
text = reFirst.ReplaceAllString(text, "1${1}")
text = reSecond.ReplaceAllString(text, "2${1}")
text = reThird.ReplaceAllString(text, "3${1}")
text = reRoman3.ReplaceAllString(text, "3${1}")
text = reRoman2.ReplaceAllString(text, "2${1}")
text = reRoman1.ReplaceAllString(text, "1${1}")
return reNotAlnum.ReplaceAllString(text, "")
}
type Resolver struct {
store *Store
canonical map[string]int
aliases map[string]int
aliasKeys []string
canonKeys []string
}
func NewResolver(s *Store) *Resolver {
r := &Resolver{store: s, canonical: map[string]int{}, aliases: map[string]int{}}
for id, name := range s.bookName {
r.canonical[normalizeBookText(name)] = id
}
for k, v := range r.canonical {
r.aliases[k] = v
}
for _, a := range s.abbrevs {
if k := normalizeBookText(a.Name); r.aliases[k] == 0 {
r.aliases[k] = a.Book
}
}
for name, extras := range extraBookAliases {
id := s.bookByName[name]
for _, extra := range extras {
r.aliases[normalizeBookText(extra)] = id
}
}
for k := range r.aliases {
r.aliasKeys = append(r.aliasKeys, k)
}
sort.Strings(r.aliasKeys)
for k := range r.canonical {
r.canonKeys = append(r.canonKeys, k)
}
sort.Strings(r.canonKeys)
return r
}
type bookMatch struct {
ID int
How string // exact | prefix | fuzzy
}
// resolveBook finds a book from a typed name. loose=false only accepts full
// names and spellings that are unmistakably a book (used for a bare word with
// no chapter number).
func (r *Resolver) resolveBook(token string, loose bool) (bookMatch, bool) {
key := normalizeBookText(token)
if key == "" {
return bookMatch{}, false
}
if !loose {
if _, ok := r.canonical[key]; ok || bareBookExtra[key] {
return bookMatch{r.aliases[key], "exact"}, true
}
// a clear misspelling of a long full name ("genisis")
if len(key) >= 6 && len(key) <= 40 && !(key[0] >= '0' && key[0] <= '9') {
if close, ok := closestMatch(key, r.canonKeys, 0.85); ok {
return bookMatch{r.canonical[close], "fuzzy"}, true
}
}
return bookMatch{}, false
}
if id, ok := r.aliases[key]; ok {
return bookMatch{id, "exact"}, true
}
// unique (or best) prefix: "deut", "1cor", "matth"
books := map[int]bool{}
for _, k := range r.aliasKeys {
if strings.HasPrefix(k, key) {
books[r.aliases[k]] = true
}
}
if len(books) > 0 {
canonHits := map[int]bool{}
for _, k := range r.canonKeys {
if strings.HasPrefix(k, key) {
canonHits[r.canonical[k]] = true
}
}
if len(canonHits) == 1 {
return bookMatch{anyKey(canonHits), "prefix"}, true
}
if len(books) == 1 {
return bookMatch{anyKey(books), "prefix"}, true
}
pool := books
if len(canonHits) > 0 {
pool = canonHits
}
best := 0
for id := range pool {
if id > best {
best = id // ambiguous: the later book, as before
}
}
return bookMatch{best, "prefix"}, true
}
if len(key) >= 4 && len(key) <= 40 {
if close, ok := closestMatch(key, r.aliasKeys, 0.78); ok {
return bookMatch{r.aliases[close], "fuzzy"}, true
}
}
return bookMatch{}, false
}
func anyKey(m map[int]bool) int {
for k := range m {
return k
}
return 0
}
// clampChapter: 0 -> 1, past the end -> the last chapter.
func (r *Resolver) clampChapter(book, chapter int) int {
last := r.store.chapterLen[book]
if chapter > last {
chapter = last
}
if chapter < 1 {
chapter = 1
}
return chapter
}
// atoi never fails: absurdly long numbers saturate.
func atoi(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
return 1_000_000
}
if n > 1_000_000 {
return 1_000_000
}
return n
}
type refPos struct{ book, chapter, verse int }
// parseRefRest: '3:16-18', '3 16', '3.16', '3-4', '3:16-4:2', '3', '3:-' -> (start, end or 0, position)
func (r *Resolver) parseRefRest(rest string, book int, previous *refPos) (start, end int64, pos refPos, ok bool) {
rest = strings.NewReplacer("–", "-", "—", "-").Replace(rest)
rest = reSepSpace.ReplaceAllString(strings.TrimSpace(rest), "$1")
base := int64(book) * 1_000_000
at := func(chapter, verse int) int64 { return base + int64(chapter)*1000 + int64(verse) }
pos.book = book
if m := reChapter.FindStringSubmatch(rest); m != nil {
n := atoi(m[1])
if previous != nil && previous.verse != 0 { // "John 3:16, 18": a verse of the same chapter
pos.chapter, pos.verse = previous.chapter, n
return at(previous.chapter, n), 0, pos, true
}
ch := r.clampChapter(book, n)
pos.chapter = ch
return at(ch, 1), at(ch, maxChapterIDSpan), pos, true
}
if m := reChapterAll.FindStringSubmatch(rest); m != nil { // "3:-" or "3-": the whole chapter
ch := r.clampChapter(book, atoi(m[1]))
pos.chapter = ch
return at(ch, 1), at(ch, maxChapterIDSpan), pos, true
}
if m := reChapterRange.FindStringSubmatch(rest); m != nil { // chapters 3 to 4
first, last := r.clampChapter(book, atoi(m[1])), r.clampChapter(book, atoi(m[2]))
if last < first {
first, last = last, first
}
pos.chapter = first
return at(first, 1), at(last, maxChapterIDSpan), pos, true
}
if m := reVerse.FindStringSubmatch(rest); m != nil {
ch := r.clampChapter(book, atoi(m[1]))
verse := max(1, atoi(m[2]))
pos.chapter, pos.verse = ch, verse
return at(ch, verse), 0, pos, true
}
if m := reVerseOn.FindStringSubmatch(rest); m != nil { // "3:16-": from verse 16 to the end
ch := r.clampChapter(book, atoi(m[1]))
pos.chapter, pos.verse = ch, atoi(m[2])
return at(ch, max(1, atoi(m[2]))), at(ch, maxChapterIDSpan), pos, true
}
if m := reVerseRange.FindStringSubmatch(rest); m != nil {
ch := r.clampChapter(book, atoi(m[1]))
first, last := max(1, atoi(m[2])), max(1, atoi(m[3]))
if last < first {
first, last = last, first
}
pos.chapter, pos.verse = ch, first
return at(ch, first), at(ch, last), pos, true
}
if m := reVerseAcrossCh.FindStringSubmatch(rest); m != nil { // 3:16-4:2 across chapters
c1, c2 := r.clampChapter(book, atoi(m[1])), r.clampChapter(book, atoi(m[3]))
s, e := at(c1, max(1, atoi(m[2]))), at(c2, max(1, atoi(m[4])))
if e < s {
return 0, 0, pos, false
}
pos.chapter, pos.verse = c1, atoi(m[2])
return s, e, pos, true
}
return 0, 0, pos, false
}
type RefRange struct{ Start, End int64 } // End == 0: a single verse
type Note struct{ Original, Name string }
// Resolved is a whole parsed query.
type Resolved struct {
Refs []RefRange
Notes []Note // short explanations of guesses that were made
Bare bool // the query was just a book name (shows its first chapter)
Label string // the name of the first book, for messages
}
// Resolve parses a whole query ('John 3:16, Romans 5:8') into verse id ranges.
// It returns nil when the text does not read as scripture references, so the
// caller can treat it as a keyword search.
func (r *Resolver) Resolve(text string, looseBare bool) *Resolved {
if strings.TrimSpace(text) == "" || len(text) > maxRefQueryLen {
return nil
}
var pieces []string
for _, p := range regexp.MustCompile(`[,;\n]+`).Split(text, -1) {
if strings.TrimSpace(p) != "" {
pieces = append(pieces, p)
}
}
if len(pieces) == 0 || len(pieces) > 20 {
return nil
}
out := &Resolved{}
var previous *refPos
for _, piece := range pieces {
var start, end int64
var pos refPos
var name string
if m := bookPieceRE.FindStringSubmatchIndex(piece); m != nil {
bi, ri := bookPieceRE.SubexpIndex("book"), bookPieceRE.SubexpIndex("rest")
token := piece[m[2*bi]:m[2*bi+1]]
hasRest := m[2*ri] >= 0
found, ok := r.resolveBook(token, hasRest || looseBare)
if !ok {
return nil
}
name = r.store.BookName(found.ID)
if found.How == "fuzzy" {
out.Notes = append(out.Notes, Note{strings.TrimSpace(token), name})
}
if !hasRest {
if len(pieces) == 1 {
out.Bare = true
}
base := int64(found.ID) * 1_000_000
start, end, pos = base+1001, base+1999, refPos{found.ID, 1, 0}
} else {
var ok bool
start, end, pos, ok = r.parseRefRest(piece[m[2*ri]:m[2*ri+1]], found.ID, nil)
if !ok {
return nil
}
}
} else if continuationRE.MatchString(piece) && previous != nil {
name = r.store.BookName(previous.book)
var ok bool
start, end, pos, ok = r.parseRefRest(piece, previous.book, previous)
if !ok {
return nil
}
} else {
return nil
}
p := pos
previous = &p
if out.Label == "" {
out.Label = name
}
out.Refs = append(out.Refs, RefRange{start, end})
}
return out
}