From cf166fc0605564ce978cb313359ac5765d77b693 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Tue, 4 Apr 2023 08:31:27 +0900 Subject: [PATCH 1/2] add fuzzySearch() --- search.go | 53 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/search.go b/search.go index 957932c..0d3d31a 100644 --- a/search.go +++ b/search.go @@ -103,28 +103,9 @@ func Search(items Items, search string, opts ...SearchOption) Matches { // Perform the search for each item. for index := start; index < end; index++ { - item := items.ItemString(index) - - // If case-insensitive, convert the item to lowercase. - if !o.caseSensitive { - item = strings.ToLower(item) - } - - // Create a slice to store the matched indexes. - matchedIndexes := make([]int, 0, len(search)) - j := 0 - - // Check for matching between the item's characters and the search string. - for i, r := range item { - if j < len(search) && r == rune(search[j]) { - matchedIndexes = append(matchedIndexes, i) - j++ - } - } - - // If all characters in the search string match, add the match to the local matches. - if j == len(search) { - m := Match{Str: items.ItemString(index), Index: index, MatchedIndexes: matchedIndexes} + m, ok := fuzzySearch(items.ItemString(index), search, o) + if ok { + m.Index = index localMatches = append(localMatches, m) } } @@ -152,3 +133,31 @@ func Search(items Items, search string, opts ...SearchOption) Matches { result.sort() return result } + +func fuzzySearch(str, search string, o searchOption) (Match, bool) { + item := str + + // If case-insensitive, convert the item to lowercase. + if !o.caseSensitive { + item = strings.ToLower(item) + } + + // Create a slice to store the matched indexes. + matchedIndexes := make([]int, 0, len(search)) + j := 0 + + // Check for matching between the item's characters and the search string. + for i, r := range item { + if j < len(search) && r == rune(search[j]) { + matchedIndexes = append(matchedIndexes, i) + j++ + } + } + + // Returns Match if all characters in the search string match. + if j == len(search) { + return Match{Str: str, MatchedIndexes: matchedIndexes}, true + } else { + return Match{}, false + } +} From 648094b035cd1bdbea55586511cc38df28b72ab7 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Tue, 4 Apr 2023 09:01:14 +0900 Subject: [PATCH 2/2] add unit test for fuzzySearch --- go.mod | 4 +++ go.sum | 15 ++++++++ search.go | 4 +-- search_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 search_test.go diff --git a/go.mod b/go.mod index b888347..5b0f9a2 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/charmbracelet/bubbletea v0.23.2 github.com/charmbracelet/lipgloss v0.7.1 github.com/spf13/cobra v1.6.1 + github.com/stretchr/testify v1.8.2 golang.org/x/sync v0.1.0 ) @@ -14,6 +15,7 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.17 // indirect @@ -23,9 +25,11 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f00cc33..4bf6b39 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,9 @@ github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNW github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= @@ -44,6 +47,8 @@ github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4Y github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -53,6 +58,13 @@ github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -68,5 +80,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/search.go b/search.go index 0d3d31a..4c4484d 100644 --- a/search.go +++ b/search.go @@ -134,11 +134,11 @@ func Search(items Items, search string, opts ...SearchOption) Matches { return result } -func fuzzySearch(str, search string, o searchOption) (Match, bool) { +func fuzzySearch(str, search string, option searchOption) (Match, bool) { item := str // If case-insensitive, convert the item to lowercase. - if !o.caseSensitive { + if !option.caseSensitive { item = strings.ToLower(item) } diff --git a/search_test.go b/search_test.go new file mode 100644 index 0000000..98b4a0a --- /dev/null +++ b/search_test.go @@ -0,0 +1,96 @@ +package fzf + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type fuzzySearchArgs struct { + str string + search string + option searchOption +} + +func assertFuzzySearch(t *testing.T, args fuzzySearchArgs, want Match, ok bool) { + got1, got2 := fuzzySearch(args.str, args.search, args.option) + + if ok { + assert.True(t, got2) + assert.Equal(t, want, got1) + } else { + assert.False(t, got2) + } +} + +func Test_fuzzySearch(t *testing.T) { + tests := []struct { + str string + search string + matchedIndexes []int + }{ + {str: "abc", search: "", matchedIndexes: []int{}}, + {str: "abc", search: "a", matchedIndexes: []int{0}}, + {str: "abc", search: "ab", matchedIndexes: []int{0, 1}}, + {str: "abc", search: "ac", matchedIndexes: []int{0, 2}}, + {str: "abc", search: "abc", matchedIndexes: []int{0, 1, 2}}, + {str: "abc", search: "b", matchedIndexes: []int{1}}, + {str: "abc", search: "bc", matchedIndexes: []int{1, 2}}, + {str: "abc", search: "c", matchedIndexes: []int{2}}, + {str: "abc", search: "cba"}, + {str: "abc", search: "d"}, + {str: "abc", search: "abcd"}, + + {str: "xaxbxc", search: "a", matchedIndexes: []int{1}}, + {str: "xaxbxc", search: "ab", matchedIndexes: []int{1, 3}}, + {str: "xaxbxc", search: "ac", matchedIndexes: []int{1, 5}}, + {str: "xaxbxc", search: "abc", matchedIndexes: []int{1, 3, 5}}, + {str: "xaxbxc", search: "b", matchedIndexes: []int{3}}, + {str: "xaxbxc", search: "bc", matchedIndexes: []int{3, 5}}, + {str: "xaxbxc", search: "c", matchedIndexes: []int{5}}, + {str: "xaxbxc", search: "cba"}, + {str: "xaxbxc", search: "d"}, + {str: "xaxbxc", search: "abcd"}, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + assertFuzzySearch( + t, + fuzzySearchArgs{str: tt.str, search: tt.search}, + Match{Str: tt.str, MatchedIndexes: tt.matchedIndexes}, + tt.matchedIndexes != nil, + ) + }) + } +} + +func Test_fuzzySearch_caseSensitive(t *testing.T) { + tests := []struct { + str string + search string + matchedIndexes []int + }{ + {str: "abc", search: "abc", matchedIndexes: []int{0, 1, 2}}, + {str: "abc", search: "Abc"}, + {str: "abc", search: "ABC"}, + + {str: "Abc", search: "abc"}, + {str: "Abc", search: "Abc", matchedIndexes: []int{0, 1, 2}}, + {str: "Abc", search: "ABC"}, + + {str: "ABC", search: "abc"}, + {str: "ABC", search: "Abc"}, + {str: "ABC", search: "ABC", matchedIndexes: []int{0, 1, 2}}, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + assertFuzzySearch( + t, + fuzzySearchArgs{str: tt.str, search: tt.search, option: searchOption{caseSensitive: true}}, + Match{Str: tt.str, MatchedIndexes: tt.matchedIndexes}, + tt.matchedIndexes != nil, + ) + }) + } +}