From 6715c22b51cd4732fc3c95ec4ee0b7e1c1b797f6 Mon Sep 17 00:00:00 2001 From: Dmitry Russkikh Date: Fri, 19 Dec 2025 00:44:50 +0300 Subject: [PATCH] Refactor public API: use option funcs instead of structs, get rid of Fix() method. Bump major version --- README.md | 144 ++++++++++++++++++++++------------- dictionary.go | 4 +- dictionary_test.go | 6 +- go.mod | 4 +- options.go | 88 --------------------- spellchecker.go | 88 --------------------- spellchecker_add.go | 117 ++++++++++++++++++++++++++++ spellchecker_add_test.go | 113 +++++++++++++++++++++++++++ spellchecker_suggest.go | 86 +++++++++++++++++++++ spellchecker_suggest_test.go | 50 ++++++++++++ spellchecker_test.go | 53 ++----------- 11 files changed, 468 insertions(+), 285 deletions(-) delete mode 100644 options.go create mode 100644 spellchecker_add.go create mode 100644 spellchecker_add_test.go create mode 100644 spellchecker_suggest.go create mode 100644 spellchecker_suggest_test.go diff --git a/README.md b/README.md index 42c4d67..f16802f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Spellchecker -[![Go Reference](https://pkg.go.dev/badge/github.com/f1monkey/spellchecker.svg)](https://pkg.go.dev/github.com/f1monkey/spellchecker) -[![CI](https://github.com/f1monkey/spellchecker/actions/workflows/test.yml/badge.svg)](https://github.com/f1monkey/spellchecker/actions/workflows/test.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/f1monkey/spellchecker.svg)](https://pkg.go.dev/github.com/f1monkey/spellchecker/v3) +[![CI](https://github.com/f1monkey/spellchecker/actions/workflows/test.yaml/badge.svg)](https://github.com/f1monkey/spellchecker/actions/workflows/test.yaml) Yet another spellchecker written in go. @@ -21,7 +21,7 @@ Yet another spellchecker written in go. ## Installation ``` -go get -v github.com/f1monkey/spellchecker/v2 +go get -v github.com/f1monkey/spellchecker/v3 ``` ## Usage @@ -29,54 +29,108 @@ go get -v github.com/f1monkey/spellchecker/v2 ### Quick start -```go +1. Initialize the spellchecker. You need to pass an alphabet: a set of allowed characters that will be used for indexing and primary word checks. (All other characters will be ignored for these operations.) -func main() { +```go // Create a new instance sc, err := spellchecker.New( "abcdefghijklmnopqrstuvwxyz1234567890", // allowed symbols, other symbols will be ignored ) - if err != nil { - panic(err) - } +``` - // The weight increases the likelihood that the word will be chosen as a correction. - weight := uint(1) +2. Add some words to the dictionary: + 1. from any `io.Reader`: + ```go + in, _ := os.Open("data/sample.txt") + sc.AddFrom(in) + ``` + 2. Or add words manually: + ```go + sc.AddMany([]string{"lock", "stock", "and", "two", "smoking"}) + sc.Add("barrels") + ``` + +3. Use the spellchecker: + 1. Check if a word is correct: + ```go + result := sc.IsCorrect("stock") + fmt.Println(result) // true + ``` + 2. Suggest corrections: + ```go + // Find up to 10 suggestions for a word + matches := sc.Suggest(nil, "rang", 10) + fmt.Println(matches) // [range, orange] + ``` +### Options - // Load data from any io.Reader - in, err := os.Open("data/sample.txt") - if err != nil { - panic(err) - } +### Options - sc.AddFrom(&spellchecker.AddOptions{Weight: weight}, in) - // OR - sc.AddFrom(nil, in) +The spellchecker supports customizable options for both searching/suggesting corrections and adding words to the dictionary. - // Add words manually - sc.Add(nil, "lock", "stock", "and", "two", "smoking", "barrels") +#### Search/Suggestion Options - // Check if a word is valid - result := sc.IsCorrect("coffee") - fmt.Println(result) // true +These options are passed to the `Suggest` method (or to `SuggestWith...` helpers). - // Correct a single word - fixed, isCorrect := sc.Fix(nil, "awepon") - fmt.Println(isCorrect) // false - fmt.Println(fixed) // weapon +- **`SuggestWithMaxErrors(maxErrors int)`** + Sets the maximum allowed edit distance (in "bits") between the input word and dictionary candidates. + - Deletion: 1 bit (e.g., "proble" → "problem") + - Insertion: 1 bit (e.g., "problemm" → "problem") + - Substitution: 2 bits (e.g., "problam" → "problem") + - Transposition: 0 bits (e.g., "problme" → "problem") - // Find up to 10 suggestions for a word - matches := sc.Suggest(nil, "rang", 10) - fmt.Println(matches) // [range, orange] + Default: `2`. + Increasing this value beyond 2 is not recommended as it can significantly degrade performance. - if len(os.Args) < 2 { - log.Fatal("dict path must be provided") - } +- **`SuggestWithFilterFunc(f FilterFunc)`** + Replaces the default scoring/filtering function with a custom one. + The function receives: + - `src`: runes of the input word + - `candidate`: runes of the dictionary word + - `count`: frequency count of the candidate in the dictionary + + It must return: + - a `float64` score (higher = better suggestion) + - a `bool` indicating whether the candidate should be kept + + The default filter uses Levenshtein distance (with costs: insert/delete=1, substitute=1, transpose=1), filters out candidates exceeding `maxErrors`, and boosts score based on word frequency and shared prefix/suffix length. + +Example usage: +```go +matches := sc.Suggest( + "rang", + 10, + spellchecker.SuggestWithMaxErrors(1), + spellchecker.SuggestWithFilterFunc(myCustomFilter), +) ``` -### Options +#### Add Options +These options are passed to `Add`, `AddMany`, or `AddFrom`. -See [options.go](./options.go) for the list of available options. +- **`AddWithWeight(weight uint)`** + Sets the frequency weight for added word(s). Higher weight increases the chance that the word will appear higher in suggestion results. + Default: 1. +- **`AddWithSplitter(splitter bufio.SplitFunc)`** + Customizes how AddFrom(reader) splits the input stream into words. + + The default splitter: + - Uses bufio.ScanWords as base + - Converts to lowercase + - Keeps only sequences matching [-\pL]+ (letters and hyphens) + +Example: +```go +sc.AddFrom( + file, + spellchecker.AddWithWeight(10), // these words are very common + spellchecker.AddWithSplitter(customSplitter), +) + +sc.AddMany([]string{"hello", "world"}, + spellchecker.AddWithWeight(5), +) +``` ### Save/load @@ -101,26 +155,6 @@ See [options.go](./options.go) for the list of available options. } ``` -### Custom score function - -You can provide a custom scoring function if needed: - -```go - var fn spellchecker.FilterFunc = func(src, candidate []rune, cnt int) (float64, bool) { - // you can calculate Levenshtein distance here (see defaultFilterFunc in options.go for example) - - return 1.0, true // constant score - } - - sc, err := spellchecker.New("abc", spellchecker.WithFilterFunc(fn)) - if err != nil { - // handle err - } - - sc.Fix(fn, "word") -``` - - ## Benchmarks Tests are based on data from [Peter Norvig's article about spelling correction](http://norvig.com/spell-correct.html) diff --git a/dictionary.go b/dictionary.go index cfd7eb4..499c1e4 100644 --- a/dictionary.go +++ b/dictionary.go @@ -48,7 +48,7 @@ func (d *dictionary) has(word string) bool { } // add puts the word to the dictionary -func (d *dictionary) add(word string, n uint) (uint32, error) { +func (d *dictionary) add(word string, n uint) uint32 { id := d.nextID() d.ids[word] = id @@ -59,7 +59,7 @@ func (d *dictionary) add(word string, n uint) (uint32, error) { key := sum(d.alphabet.encode(wordRunes)) d.index[key] = append(d.index[key], id) - return id, nil + return id } // inc increase word occurence counter diff --git a/dictionary_test.go b/dictionary_test.go index 0311fa2..2b44f6c 100644 --- a/dictionary_test.go +++ b/dictionary_test.go @@ -27,16 +27,14 @@ func Test_dictionary_add(t *testing.T) { dict, err := newDictionary(DefaultAlphabet) require.NoError(t, err) - id, err := dict.add("qwe", 1) - require.NoError(t, err) + id := dict.add("qwe", 1) require.Equal(t, uint32(1), id) require.Equal(t, uint(1), dict.counts[id]) require.Equal(t, []rune("qwe"), dict.words[id]) require.Equal(t, 1, len(dict.ids)) require.Len(t, dict.index, 1) - id, err = dict.add("asd", 2) - require.NoError(t, err) + id = dict.add("asd", 2) require.Equal(t, uint32(2), id) require.Equal(t, uint(2), dict.counts[id]) require.Equal(t, []rune("asd"), dict.words[id]) diff --git a/go.mod b/go.mod index 7f609b7..f224cc3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/f1monkey/spellchecker/v2 +module github.com/f1monkey/spellchecker/v3 -go 1.24 +go 1.25 require ( github.com/agext/levenshtein v1.2.3 diff --git a/options.go b/options.go deleted file mode 100644 index c23019d..0000000 --- a/options.go +++ /dev/null @@ -1,88 +0,0 @@ -package spellchecker - -import ( - "bufio" - "bytes" - "math" - "regexp" - - "github.com/agext/levenshtein" -) - -const DefaultMaxErrors = 2 - -type FilterFunc func(src, candidate []rune, count uint) (float64, bool) - -type SearchOptions struct { - // MaxErrors — the maximum allowed difference in bits - // between the "search word" and a "dictionary word". - // - deletion is a 1-bit change (proble → problem) - // - insertion is a 1-bit change (problemm → problem) - // - substitution is a 2-bit change (problam → problem) - // - transposition is a 0-bit change (problme → problem) - // - // It is not recommended to set this value greater than 2, - // as it can significantly affect performance. - MaxErrors int - - // FilterFunc compares the source word with a candidate word. - // It returns the candidate's score and a boolean flag. - // If the flag is false, the candidate will be completely filtered out. - FilterFunc FilterFunc -} - -var defaultSearchOptions = &SearchOptions{ - MaxErrors: DefaultMaxErrors, - FilterFunc: defaultFilterFunc(DefaultMaxErrors), -} - -type AddOptions struct { - Weight uint - // Splitter is a splitter func for AddFrom() reader - Splitter bufio.SplitFunc -} - -var defaultAddOptions = &AddOptions{ - Weight: 1, - Splitter: defaultSplitter, -} - -var wordSymbols = regexp.MustCompile(`[-\pL]+`) - -func defaultSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) { - advance, token, err = bufio.ScanWords(data, atEOF) - if err != nil { - return - } - token = bytes.ToLower(token) - - return advance, wordSymbols.Find(token), nil -} - -func defaultFilterFunc(maxErrors int) FilterFunc { - return func(src, candidate []rune, count uint) (float64, bool) { - distance, prefixLen, suffixLen := levenshtein.Calculate(src, candidate, 0, 1, 1, 1) - if distance > maxErrors { - return 0, false - } - - mult := math.Log1p(float64(count)) * math.Pow(1.5, float64(prefixLen+suffixLen)) - - return 1 / (1 + float64(distance*distance)) * mult, true - } -} - -func applyDefaults(opts *SearchOptions) *SearchOptions { - if opts == nil { - opts = defaultSearchOptions - } else { - if opts.MaxErrors == 0 { - opts.MaxErrors = DefaultMaxErrors - } - if opts.FilterFunc == nil { - opts.FilterFunc = defaultFilterFunc(opts.MaxErrors) - } - } - - return opts -} diff --git a/spellchecker.go b/spellchecker.go index b9df6d0..dc39e17 100644 --- a/spellchecker.go +++ b/spellchecker.go @@ -1,7 +1,6 @@ package spellchecker import ( - "io" "sync" ) @@ -22,53 +21,6 @@ func New(alphabet string) (*Spellchecker, error) { return result, nil } -// AddFrom reads input, splits it with spellchecker splitter func and adds words to the dictionary -func (m *Spellchecker) AddFrom(opts *AddOptions, input io.Reader) error { - if opts == nil { - opts = defaultAddOptions - } - - words := make([]string, 1000) - i := 0 - for item := range readInput(input, opts.Splitter) { - if item.err != nil { - return item.err - } - - if i == len(words) { - m.Add(opts, words...) - i = 0 - } - words[i] = item.word - i++ - } - - if i > 0 { - m.Add(opts, words[:i]...) - } - - return nil -} - -// Add adds provided words to the dictionary with a custom weight -func (m *Spellchecker) Add(opts *AddOptions, words ...string) { - m.mtx.Lock() - defer m.mtx.Unlock() - - if opts == nil { - opts = defaultAddOptions - } - - for _, word := range words { - if id := m.dict.id(word); id > 0 { - m.dict.inc(id, opts.Weight) - continue - } - - m.dict.add(word, opts.Weight) - } -} - // IsCorrect check if provided word is in the dictionary func (s *Spellchecker) IsCorrect(word string) bool { s.mtx.RLock() @@ -76,43 +28,3 @@ func (s *Spellchecker) IsCorrect(word string) bool { return s.dict.has(word) } - -func (s *Spellchecker) Fix(opts *SearchOptions, word string) (string, bool) { - s.mtx.RLock() - defer s.mtx.RUnlock() - - if s.dict.has(word) { - return word, true - } - - opts = applyDefaults(opts) - - hits := s.dict.find(word, 1, opts.MaxErrors, opts.FilterFunc) - if len(hits) == 0 { - return word, false - } - - return hits[0].Value, false -} - -type SuggestionResult struct { - ExactMatch bool // if true, the word is correct - Suggestions []Match -} - -// Suggest find top n suggestions for the word. -// Returns spellchecker scores along with words -func (s *Spellchecker) Suggest(opts *SearchOptions, word string, n int) SuggestionResult { - s.mtx.RLock() - defer s.mtx.RUnlock() - - if s.dict.has(word) { - return SuggestionResult{ExactMatch: true} - } - - opts = applyDefaults(opts) - - return SuggestionResult{ - Suggestions: s.dict.find(word, n, opts.MaxErrors, opts.FilterFunc), - } -} diff --git a/spellchecker_add.go b/spellchecker_add.go new file mode 100644 index 0000000..4d9ef1a --- /dev/null +++ b/spellchecker_add.go @@ -0,0 +1,117 @@ +package spellchecker + +import ( + "bufio" + "bytes" + "io" + "regexp" +) + +type AddOptionFunc func(opts *addOptions) + +// AddWithWeight sets weight for added words. +// The weight increases the likelihood that the word will be chosen as a correction. +func AddWithWeight(weight uint) AddOptionFunc { + return func(opts *addOptions) { + opts.weight = weight + } +} + +// AddWithSplitter sets a splitter func for AddFrom() reader +func AddWithSplitter(splitter bufio.SplitFunc) AddOptionFunc { + return func(opts *addOptions) { + opts.splitter = splitter + } +} + +// AddFrom reads input, splits it with spellchecker splitter func and adds words to the dictionary +func (m *Spellchecker) AddFrom(input io.Reader, opts ...AddOptionFunc) error { + addOpts := defaultAddOptions + for _, o := range opts { + o(&addOpts) + } + + words := make([]string, 1000) + i := 0 + for item := range readInput(input, addOpts.splitter) { + if item.err != nil { + return item.err + } + + if i == len(words) { + m.addMany(words, addOpts.weight) + i = 0 + } + words[i] = item.word + i++ + } + + if i > 0 { + m.addMany(words, addOpts.weight) + } + + return nil +} + +// AddMany adds provided words to the dictionary +func (m *Spellchecker) AddMany(words []string, opts ...AddOptionFunc) { + m.mtx.Lock() + defer m.mtx.Unlock() + + addOpts := defaultAddOptions + for _, o := range opts { + o(&addOpts) + } + + m.addMany(words, addOpts.weight) +} + +// Add adds provided word to the dictionary +func (m *Spellchecker) Add(word string, opts ...AddOptionFunc) { + m.mtx.Lock() + defer m.mtx.Unlock() + + addOpts := defaultAddOptions + for _, o := range opts { + o(&addOpts) + } + + m.add(word, addOpts.weight) +} + +func (m *Spellchecker) addMany(words []string, weight uint) { + for _, word := range words { + m.add(word, weight) + } +} + +func (m *Spellchecker) add(word string, weight uint) { + if id := m.dict.id(word); id > 0 { + m.dict.inc(id, weight) + return + } + + m.dict.add(word, weight) +} + +type addOptions struct { + weight uint + splitter bufio.SplitFunc +} + +var defaultAddOptions = addOptions{ + weight: 1, + splitter: defaultSplitter, +} + +var wordSymbols = regexp.MustCompile(`[-\pL]+`) + +func defaultSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) { + advance, token, err = bufio.ScanWords(data, atEOF) + if err != nil { + return + } + token = bytes.ToLower(token) + + return advance, wordSymbols.Find(token), nil +} diff --git a/spellchecker_add_test.go b/spellchecker_add_test.go new file mode 100644 index 0000000..6a66ce7 --- /dev/null +++ b/spellchecker_add_test.go @@ -0,0 +1,113 @@ +package spellchecker + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Spellchecker_AddFrom(t *testing.T) { + t.Run("no options", func(t *testing.T) { + sc, err := New("abc") + require.NoError(t, err) + + buf := bytes.NewBuffer([]byte("hello world")) + + err = sc.AddFrom(buf) + require.NoError(t, err) + + require.True(t, sc.IsCorrect("world")) + + require.Equal(t, uint(1), sc.dict.counts[1]) + require.Equal(t, uint(1), sc.dict.counts[2]) + }) + + t.Run("custom weight", func(t *testing.T) { + sc, err := New("abc") + require.NoError(t, err) + + buf := bytes.NewBuffer([]byte("hello world")) + + err = sc.AddFrom(buf, AddWithWeight(2)) + require.NoError(t, err) + + require.True(t, sc.IsCorrect("hello")) + require.True(t, sc.IsCorrect("world")) + + require.Equal(t, uint(2), sc.dict.counts[1]) + require.Equal(t, uint(2), sc.dict.counts[2]) + }) + + t.Run("custom splitter", func(t *testing.T) { + sc, err := New("abc") + require.NoError(t, err) + + buf := bytes.NewBuffer([]byte("hello world")) + + err = sc.AddFrom(buf, AddWithSplitter(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) > 0 { + return len(data), data, nil + } + + return 0, nil, nil + })) + require.NoError(t, err) + + require.False(t, sc.IsCorrect("hello")) + require.False(t, sc.IsCorrect("world")) + require.True(t, sc.IsCorrect("hello world")) + }) +} + +func Test_Spellchecker_AddMany(t *testing.T) { + t.Run("no options", func(t *testing.T) { + sc, err := New("abc") + require.NoError(t, err) + + sc.AddMany([]string{"hello", "world"}) + + require.True(t, sc.IsCorrect("hello")) + require.True(t, sc.IsCorrect("world")) + + require.Equal(t, uint(1), sc.dict.counts[1]) + require.Equal(t, uint(1), sc.dict.counts[2]) + }) + + t.Run("custom weight", func(t *testing.T) { + sc, err := New("abc") + require.NoError(t, err) + + sc.AddMany([]string{"hello", "world"}, AddWithWeight(2)) + + require.True(t, sc.IsCorrect("hello")) + require.True(t, sc.IsCorrect("world")) + + require.Equal(t, uint(2), sc.dict.counts[1]) + require.Equal(t, uint(2), sc.dict.counts[2]) + }) +} + +func Test_Spellchecker_Add(t *testing.T) { + t.Run("no options", func(t *testing.T) { + sc, err := New("abc") + require.NoError(t, err) + + sc.Add("hello") + + require.True(t, sc.IsCorrect("hello")) + + require.Equal(t, uint(1), sc.dict.counts[1]) + }) + + t.Run("custom weight", func(t *testing.T) { + sc, err := New("abc") + require.NoError(t, err) + + sc.Add("hello", AddWithWeight(2)) + + require.True(t, sc.IsCorrect("hello")) + + require.Equal(t, uint(2), sc.dict.counts[1]) + }) +} diff --git a/spellchecker_suggest.go b/spellchecker_suggest.go new file mode 100644 index 0000000..91d4aea --- /dev/null +++ b/spellchecker_suggest.go @@ -0,0 +1,86 @@ +package spellchecker + +import ( + "math" + + "github.com/agext/levenshtein" +) + +const DefaultMaxErrors = 2 + +// FilterFunc compares the source word with a candidate word. +// It returns the candidate's score and a boolean flag. +// If the flag is false, the candidate will be completely filtered out. +type FilterFunc func(src, candidate []rune, count uint) (float64, bool) + +type SearchOptionFunc func(opts *searchOptions) + +// SuggestWithMaxErrors sets the maximum allowed difference in bits +// between the "search word" and a "dictionary word". +// - deletion is a 1-bit change (proble → problem) +// - insertion is a 1-bit change (problemm → problem) +// - substitution is a 2-bit change (problam → problem) +// - transposition is a 0-bit change (problme → problem) +// +// It is not recommended to set this value greater than 2, +// as it can significantly affect performance. +func SuggestWithMaxErrors(maxErrors int) SearchOptionFunc { + return func(opts *searchOptions) { + opts.maxErrors = maxErrors + } +} + +// SuggestWithFilterFunc set a FilterFunc +func SuggestWithFilterFunc(f FilterFunc) SearchOptionFunc { + return func(opts *searchOptions) { + opts.filterFunc = f + } +} + +type SuggestionResult struct { + ExactMatch bool // if true, the word is correct + Suggestions []Match +} + +// Suggest find top n suggestions for the word. +// Returns spellchecker scores along with words +func (s *Spellchecker) Suggest(word string, n int, opts ...SearchOptionFunc) SuggestionResult { + s.mtx.RLock() + defer s.mtx.RUnlock() + + if s.dict.has(word) { + return SuggestionResult{ExactMatch: true} + } + + searchOpts := defaultSearchOptions + for _, o := range opts { + o(&searchOpts) + } + + return SuggestionResult{ + Suggestions: s.dict.find(word, n, searchOpts.maxErrors, searchOpts.filterFunc), + } +} + +type searchOptions struct { + maxErrors int + filterFunc FilterFunc +} + +var defaultSearchOptions = searchOptions{ + maxErrors: DefaultMaxErrors, + filterFunc: defaultFilterFunc(DefaultMaxErrors), +} + +func defaultFilterFunc(maxErrors int) FilterFunc { + return func(src, candidate []rune, count uint) (float64, bool) { + distance, prefixLen, suffixLen := levenshtein.Calculate(src, candidate, 0, 1, 1, 1) + if distance > maxErrors { + return 0, false + } + + mult := math.Log1p(float64(count)) * math.Pow(1.5, float64(prefixLen+suffixLen)) + + return 1 / (1 + float64(distance*distance)) * mult, true + } +} diff --git a/spellchecker_suggest_test.go b/spellchecker_suggest_test.go new file mode 100644 index 0000000..d9be0ef --- /dev/null +++ b/spellchecker_suggest_test.go @@ -0,0 +1,50 @@ +package spellchecker + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Spellchecker_SuggestScore(t *testing.T) { + t.Run("fix", func(t *testing.T) { + s := newSampleSpellchecker() + result := s.Suggest("arang", 5) + require.Equal(t, SuggestionResult{ + Suggestions: []Match{ + {Value: "orange", Score: 0.2772588722239781}, + {Value: "range", Score: 0.13862943611198905}, + }, + }, result) + }) + + t.Run("custom max errors", func(t *testing.T) { + s := newSampleSpellchecker() + result := s.Suggest("arang", 5, SuggestWithMaxErrors(1)) + require.Equal(t, SuggestionResult{ + Suggestions: []Match{ + {Value: "range", Score: 0.13862943611198905}, + }, + }, result) + + result = s.Suggest("arang", 5, SuggestWithMaxErrors(2)) + require.Equal(t, SuggestionResult{ + Suggestions: []Match{ + {Value: "orange", Score: 0.2772588722239781}, + {Value: "range", Score: 0.13862943611198905}, + }, + }, result) + }) + + t.Run("valid word", func(t *testing.T) { + s := newSampleSpellchecker() + result := s.Suggest("orange", 5) + require.Equal(t, SuggestionResult{ExactMatch: true}, result) + }) + + t.Run("unknown word", func(t *testing.T) { + s := newSampleSpellchecker() + result := s.Suggest("qwerty", 5) + require.Equal(t, SuggestionResult{Suggestions: []Match{}}, result) + }) +} diff --git a/spellchecker_test.go b/spellchecker_test.go index 60e3d40..7b8ddbf 100644 --- a/spellchecker_test.go +++ b/spellchecker_test.go @@ -46,7 +46,7 @@ func newFullSpellchecker() *Spellchecker { panic(err) } - err = s.AddFrom(nil, f) + err = s.AddFrom(f) if err != nil { panic(err) } @@ -65,7 +65,7 @@ func newSampleSpellchecker() *Spellchecker { panic(err) } - err = s.AddFrom(nil, f) + err = s.AddFrom(f) if err != nil { panic(err) } @@ -88,12 +88,12 @@ func Benchmark_Spellchecker_IsCorrect(b *testing.B) { } } -func Benchmark_Spellchecker_Fix_3(b *testing.B) { +func Benchmark_Spellchecker_Suggest_3(b *testing.B) { m := loadFullSpellchecker() b.ResetTimer() for i := 0; i < b.N; i++ { - m.Fix(nil, "tee") + m.Suggest("tee", 5) } } @@ -102,7 +102,7 @@ func Benchmark_Spellchecker_Fix_6_Transposition(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - m.Fix(nil, "oragne") + m.Suggest("oragne", 5) } } @@ -111,7 +111,7 @@ func Benchmark_Spellchecker_Fix_6_Replacement(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - m.Fix(nil, "problam") + m.Suggest("problam", 5) } } @@ -170,7 +170,7 @@ func benchmarkNorvig(b *testing.B, dataPath string) { } b.StartTimer() - result := m.Suggest(nil, word, 10) + result := m.Suggest(word, 10) b.StopTimer() if i == 0 { @@ -215,42 +215,3 @@ func Test_Spellchecker_IsCorrect(t *testing.T) { assert.True(t, s.IsCorrect("orange")) assert.False(t, s.IsCorrect("car")) } - -func Test_Spellchecker_Fix(t *testing.T) { - s := newSampleSpellchecker() - result, isCorrect := s.Fix(nil, "problam") - require.False(t, isCorrect) - require.Equal(t, "problem", result) -} - -func Test_Spellchecker_Fix_CustomOptions(t *testing.T) { - s := newSampleSpellchecker() - result, isCorrect := s.Fix(&SearchOptions{MaxErrors: 2}, "problam") - require.False(t, isCorrect) - require.Equal(t, "problem", result) -} - -func Test_Spellchecker_SuggestScore(t *testing.T) { - t.Run("fix", func(t *testing.T) { - s := newSampleSpellchecker() - result := s.Suggest(nil, "arang", 5) - require.Equal(t, SuggestionResult{ - Suggestions: []Match{ - {Value: "orange", Score: 0.2772588722239781}, - {Value: "range", Score: 0.13862943611198905}, - }, - }, result) - }) - - t.Run("valid word", func(t *testing.T) { - s := newSampleSpellchecker() - result := s.Suggest(nil, "orange", 5) - require.Equal(t, SuggestionResult{ExactMatch: true}, result) - }) - - t.Run("unknown word", func(t *testing.T) { - s := newSampleSpellchecker() - result := s.Suggest(nil, "qwerty", 5) - require.Equal(t, SuggestionResult{Suggestions: []Match{}}, result) - }) -}