diff --git a/filter.go b/filter.go index 583674a..3bd0bad 100644 --- a/filter.go +++ b/filter.go @@ -7,50 +7,49 @@ type Filter func([]Suggest, string, bool) []Suggest // FilterHasPrefix checks whether the string completions.Text begins with sub. func FilterHasPrefix(completions []Suggest, sub string, ignoreCase bool) []Suggest { - if sub == "" { - return completions - } - if ignoreCase { - sub = strings.ToUpper(sub) - } - - ret := make([]Suggest, 0, len(completions)) - for i := range completions { - c := completions[i].Text - if ignoreCase { - c = strings.ToUpper(c) - } - if strings.HasPrefix(c, sub) { - ret = append(ret, completions[i]) - } - } - return ret + return filterCommon(completions, sub, ignoreCase, strings.HasPrefix) } // FilterHasSuffix checks whether the completion.Text ends with sub. func FilterHasSuffix(completions []Suggest, sub string, ignoreCase bool) []Suggest { - if sub == "" { - return completions - } - if ignoreCase { - sub = strings.ToUpper(sub) - } - - ret := make([]Suggest, 0, len(completions)) - for i := range completions { - c := completions[i].Text - if ignoreCase { - c = strings.ToUpper(c) - } - if strings.HasSuffix(c, sub) { - ret = append(ret, completions[i]) - } - } - return ret + return filterCommon(completions, sub, ignoreCase, strings.HasSuffix) } // FilterContains checks whether the completion.Text contains sub. func FilterContains(completions []Suggest, sub string, ignoreCase bool) []Suggest { + return filterCommon(completions, sub, ignoreCase, strings.Contains) +} + +// FilterFuzzy checks whether the completion.Text fuzzy matches sub. +// Fuzzy searching for "dog" is equivalent to "*d*o*g*". This search term +// would match, for example, "Good food is gone" +// ^ ^ ^ +func FilterFuzzy(completions []Suggest, sub string, ignoreCase bool) []Suggest { + return filterCommon(completions, sub, ignoreCase, fuzzyMatch) +} + +func fuzzyMatch(s, sub string) bool { + sChars := []rune(s) + subChars := []rune(sub) + sIdx := 0 + + for _, c := range subChars { + found := false + for ; sIdx < len(sChars); sIdx++ { + if sChars[sIdx] == c { + found = true + sIdx++ + break + } + } + if !found { + return false + } + } + return true +} + +func filterCommon(completions []Suggest, sub string, ignoreCase bool, test func(string, string) bool) []Suggest { if sub == "" { return completions } @@ -64,7 +63,7 @@ func FilterContains(completions []Suggest, sub string, ignoreCase bool) []Sugges if ignoreCase { c = strings.ToUpper(c) } - if strings.Contains(c, sub) { + if test(c, sub) { ret = append(ret, completions[i]) } } diff --git a/filter_test.go b/filter_test.go index afcda13..b0298a1 100644 --- a/filter_test.go +++ b/filter_test.go @@ -101,6 +101,35 @@ func TestFilter(t *testing.T) { {Text: "ABCDE"}, }, }, + { + scenario: "Fuzzy don't ignore case", + filter: FilterFuzzy, + list: []Suggest{ + {Text: "abcde"}, + {Text: "fcdej"}, + {Text: "ABCDE"}, + }, + substr: "ae", + ignoreCase: false, + expected: []Suggest{ + {Text: "abcde"}, + }, + }, + { + scenario: "Fuzzy ignore case", + filter: FilterFuzzy, + list: []Suggest{ + {Text: "abcde"}, + {Text: "fcdej"}, + {Text: "ABCDE"}, + }, + substr: "ae", + ignoreCase: true, + expected: []Suggest{ + {Text: "abcde"}, + {Text: "ABCDE"}, + }, + }, } for _, s := range scenarioTable { @@ -109,3 +138,27 @@ func TestFilter(t *testing.T) { } } } + +func TestFuzzy(t *testing.T) { + tests := []struct { + s string + sub string + match bool + }{ + {"dog house", "dog", true}, + {"dog house", "", true}, + {"", "", true}, + {"this is much longer", "hhg", true}, + {"this is much longer", "hhhg", false}, + {"long", "longer", false}, + {"can we do unicode 文字 with this 今日", "文字今日", true}, + {"can we do unicode 文字 with this 今日", "d文字tt今日", true}, + {"can we do unicode 文字 with this 今日", "d文字ttt今日", false}, + } + + for _, test := range tests { + if fuzzyMatch(test.s, test.sub) != test.match { + t.Errorf("fuzzymatch, %s in %s: expected %v, got %v", test.sub, test.s, test.match, !test.match) + } + } +}