254 lines
5.6 KiB
Go
254 lines
5.6 KiB
Go
package kjv
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// Speller checks and suggests spellings from kjv.dic (a plain word list) and
|
|
// kjv.aff (which only adds a TRY order and REP replacement hints), so no C
|
|
// library is needed.
|
|
type Speller struct {
|
|
words map[string]bool // exact entries
|
|
byLower map[string][]string // lowercase -> entries
|
|
list []string // lowercase entries, dictionary order, unique
|
|
try []rune
|
|
rep [][2]string
|
|
}
|
|
|
|
func NewSpeller(dic, aff []byte) *Speller {
|
|
s := &Speller{words: map[string]bool{}, byLower: map[string][]string{}}
|
|
lines := strings.Split(string(dic), "\n")
|
|
seen := map[string]bool{}
|
|
for i, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if i == 0 || line == "" { // first line is the entry count
|
|
continue
|
|
}
|
|
if k := strings.IndexByte(line, '/'); k >= 0 {
|
|
line = line[:k]
|
|
}
|
|
s.words[line] = true
|
|
low := strings.ToLower(line)
|
|
s.byLower[low] = append(s.byLower[low], line)
|
|
if !seen[low] {
|
|
seen[low] = true
|
|
s.list = append(s.list, low)
|
|
}
|
|
}
|
|
for _, line := range strings.Split(string(aff), "\n") {
|
|
f := strings.Fields(line)
|
|
switch {
|
|
case len(f) == 2 && f[0] == "TRY":
|
|
s.try = []rune(f[1])
|
|
case len(f) == 3 && f[0] == "REP":
|
|
s.rep = append(s.rep, [2]string{f[1], strings.ReplaceAll(f[2], "_", " ")})
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func stripSyntax(w string) string {
|
|
return strings.Map(func(r rune) rune {
|
|
if r == '+' || r == '^' || r == '*' {
|
|
return -1
|
|
}
|
|
return r
|
|
}, w)
|
|
}
|
|
|
|
func hasLetter(w string) bool {
|
|
for _, r := range w {
|
|
if unicode.IsLetter(r) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *Speller) Spell(word string) bool {
|
|
word = stripSyntax(word)
|
|
if !hasLetter(word) || s.words[word] {
|
|
return true
|
|
}
|
|
low := strings.ToLower(word)
|
|
if low != word && (word == strings.ToUpper(word) || word == capitalize(low)) {
|
|
return len(s.byLower[low]) > 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
func capitalize(w string) string {
|
|
r := []rune(w)
|
|
if len(r) == 0 {
|
|
return w
|
|
}
|
|
return strings.ToUpper(string(r[:1])) + strings.ToLower(string(r[1:]))
|
|
}
|
|
|
|
func (s *Speller) known(low string) []string { return s.byLower[low] }
|
|
|
|
// Suggest returns likely corrections, best first.
|
|
func (s *Speller) Suggest(word string) []string {
|
|
word = stripSyntax(word)
|
|
low := strings.ToLower(word)
|
|
if low == "" {
|
|
return nil
|
|
}
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
add := func(cands ...string) {
|
|
for _, c := range cands {
|
|
if !seen[c] {
|
|
seen[c] = true
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
}
|
|
addLower := func(l string) { // dictionary form(s) of a lowercase candidate
|
|
if l == low {
|
|
return
|
|
}
|
|
add(s.known(l)...)
|
|
}
|
|
if low != word { // wrong capitalization of a known word
|
|
add(s.known(low)...)
|
|
}
|
|
// replacement hints from the .aff file, applied at each occurrence
|
|
for _, r := range s.rep {
|
|
for at := 0; ; {
|
|
i := strings.Index(low[at:], r[0])
|
|
if i < 0 {
|
|
break
|
|
}
|
|
i += at
|
|
cand := low[:i] + r[1] + low[i+len(r[0]):]
|
|
if strings.Contains(cand, " ") {
|
|
if p := strings.Fields(cand); len(p) == 2 && len(s.known(p[0])) > 0 && len(s.known(p[1])) > 0 {
|
|
add(p[0] + " " + p[1])
|
|
}
|
|
} else {
|
|
addLower(cand)
|
|
}
|
|
at = i + 1
|
|
}
|
|
}
|
|
rs := []rune(low)
|
|
// one edit away: swap, delete, replace, insert
|
|
for i := 0; i+1 < len(rs); i++ {
|
|
c := append([]rune(nil), rs...)
|
|
c[i], c[i+1] = c[i+1], c[i]
|
|
addLower(string(c))
|
|
}
|
|
for i := range rs {
|
|
addLower(string(rs[:i]) + string(rs[i+1:]))
|
|
}
|
|
for i := range rs {
|
|
for _, t := range s.try {
|
|
addLower(string(rs[:i]) + string(t) + string(rs[i+1:]))
|
|
}
|
|
}
|
|
for i := 0; i <= len(rs); i++ {
|
|
for _, t := range s.try {
|
|
addLower(string(rs[:i]) + string(t) + string(rs[i:]))
|
|
}
|
|
}
|
|
// two words run together ("lordgod")
|
|
for i := 1; i < len(rs); i++ {
|
|
a, b := string(rs[:i]), string(rs[i:])
|
|
if len(a) > 1 && len(b) > 1 && len(s.known(a)) > 0 && len(s.known(b)) > 0 {
|
|
add(a + " " + b)
|
|
}
|
|
}
|
|
// still little to offer: anything within two edits, closest first
|
|
if len(out) < 3 {
|
|
type cand struct {
|
|
w string
|
|
dist int
|
|
pre int
|
|
}
|
|
var cs []cand
|
|
for _, w := range s.list {
|
|
if abs(len(w)-len(low)) > 2 || w == low {
|
|
continue
|
|
}
|
|
if d := editDistance(low, w, 2); d <= 2 {
|
|
cs = append(cs, cand{w, d, commonPrefix(low, w)})
|
|
}
|
|
}
|
|
sort.SliceStable(cs, func(i, j int) bool {
|
|
if cs[i].dist != cs[j].dist {
|
|
return cs[i].dist < cs[j].dist
|
|
}
|
|
return cs[i].pre > cs[j].pre
|
|
})
|
|
for _, c := range cs {
|
|
addLower(c.w)
|
|
if len(out) >= 8 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(out) > 10 {
|
|
out = out[:10]
|
|
}
|
|
if word == strings.ToUpper(word) && len(word) > 1 {
|
|
for i := range out {
|
|
out[i] = strings.ToUpper(out[i])
|
|
}
|
|
} else if word == capitalize(low) && word != low {
|
|
for i := range out {
|
|
out[i] = capitalize(out[i])
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func abs(n int) int {
|
|
if n < 0 {
|
|
return -n
|
|
}
|
|
return n
|
|
}
|
|
|
|
func commonPrefix(a, b string) int {
|
|
n := 0
|
|
for n < len(a) && n < len(b) && a[n] == b[n] {
|
|
n++
|
|
}
|
|
return n
|
|
}
|
|
|
|
// editDistance is optimal-string-alignment distance, giving up above limit.
|
|
func editDistance(a, b string, limit int) int {
|
|
ra, rb := []rune(a), []rune(b)
|
|
prev2 := make([]int, len(rb)+1)
|
|
prev := make([]int, len(rb)+1)
|
|
cur := make([]int, len(rb)+1)
|
|
for j := range prev {
|
|
prev[j] = j
|
|
}
|
|
for i := 1; i <= len(ra); i++ {
|
|
cur[0] = i
|
|
best := cur[0]
|
|
for j := 1; j <= len(rb); j++ {
|
|
cost := 1
|
|
if ra[i-1] == rb[j-1] {
|
|
cost = 0
|
|
}
|
|
v := min(prev[j]+1, cur[j-1]+1, prev[j-1]+cost)
|
|
if i > 1 && j > 1 && ra[i-1] == rb[j-2] && ra[i-2] == rb[j-1] {
|
|
v = min(v, prev2[j-2]+1)
|
|
}
|
|
cur[j] = v
|
|
best = min(best, v)
|
|
}
|
|
if best > limit {
|
|
return limit + 1
|
|
}
|
|
prev2, prev, cur = prev, cur, prev2
|
|
}
|
|
return prev[len(rb)]
|
|
}
|