Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

add unit test for search #38

Merged
merged 2 commits into from Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions go.mod
Expand Up @@ -7,13 +7,15 @@ 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
)

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
Expand All @@ -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
)
15 changes: 15 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
53 changes: 31 additions & 22 deletions search.go
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -152,3 +133,31 @@ func Search(items Items, search string, opts ...SearchOption) Matches {
result.sort()
return result
}

func fuzzySearch(str, search string, option searchOption) (Match, bool) {
item := str

// If case-insensitive, convert the item to lowercase.
if !option.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
}
}
96 changes: 96 additions & 0 deletions 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,
)
})
}
}