Skip to content

Commit

Permalink
Use vscode fuzzy search algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
sandro-h committed Oct 19, 2021
1 parent 4d9cfea commit c6dd079
Show file tree
Hide file tree
Showing 10 changed files with 621 additions and 175 deletions.
136 changes: 136 additions & 0 deletions fuzzy/fuzzy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package fuzzy

import "sort"

// MatchRange describes the start and end index of a match in the target string.
type MatchRange struct {
Start int
End int
}

// Match describes a fuzzy search match.
type Match struct {
// The matched string.
Str string
// The index of the matched string in the supplied slice.
Index int
// The indexes of matched ranges. Useful for highlighting matches.
MatchedRanges []MatchRange
// Score used to rank matches
Score int
}

// MultiMatch describes a match in one or both target lists by SearchFuzzyMulti.
type MultiMatch struct {
Index int
Score int
Match1 Match
Match2 Match
}

type byIndex []Match
type byMultiScore []MultiMatch

func (a byIndex) Len() int { return len(a) }
func (a byIndex) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byIndex) Less(i, j int) bool { return a[i].Index < a[j].Index }

func (a byMultiScore) Len() int { return len(a) }
func (a byMultiScore) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byMultiScore) Less(i, j int) bool { return a[i].Score >= a[j].Score }

// SearchFuzzyMulti searches for source in multiple target lists using a fuzzy
// algorithm. Matches with the same index in both targets are merged. If not, a resulting
// match may only contain a match in the first or second target list.
// This is meant to be used to search a list of objects where multiple fields of an object
// should be searched.
// The result is ordered from best to worst fitting match.
func SearchFuzzyMulti(source string, targets1 []string, targets2 []string) []MultiMatch {
NullMatch := Match{Index: -1}

matches1 := SearchFuzzy(source, targets1)
matches2 := SearchFuzzy(source, targets2)
sort.Stable(byIndex(matches1))
sort.Stable(byIndex(matches2))
var combined []MultiMatch

k2 := 0
for _, m1 := range matches1 {
for k2 < len(matches2) && matches2[k2].Index < m1.Index {
addMultiMatch(&combined, NullMatch, matches2[k2])
k2++
}

if k2 < len(matches2) && matches2[k2].Index == m1.Index {
addMultiMatch(&combined, m1, matches2[k2])
k2++
} else {
addMultiMatch(&combined, m1, NullMatch)
}
}

for ; k2 < len(matches2); k2++ {
addMultiMatch(&combined, NullMatch, matches2[k2])
}

sort.Stable(byMultiScore(combined))
return combined
}

func addMultiMatch(combined *[]MultiMatch, m1 Match, m2 Match) {
index := 0
score := 0
if m1.Index > -1 {
index = m1.Index
score += m1.Score
}
if m2.Index > -1 {
index = m2.Index
score += m2.Score
}
*combined = append(*combined, MultiMatch{
Index: index,
Match1: m1,
Match2: m2,
Score: score,
})
}

// SearchFuzzy searches for source in the list of targets using a fuzzy
// algorithm. The result is ordered from best to worst fitting match.
func SearchFuzzy(source string, targets []string) []Match {
if source == "" {
var res []Match
for i, t := range targets {
res = append(res, Match{
Str: t,
Index: i,
Score: 0,
})
}
return res
}

return FindFuzzy(source, targets)
}

func mergeMatchPositions(positions []int) []MatchRange {
var ranges []MatchRange
var cur *MatchRange
for _, i := range positions {
if cur == nil {
cur = &MatchRange{Start: i, End: i}
} else {
if i == cur.End+1 {
cur.End = i
} else {
ranges = append(ranges, *cur)
cur = &MatchRange{Start: i, End: i}
}
}
}
if cur != nil {
ranges = append(ranges, *cur)
}
return ranges
}
35 changes: 35 additions & 0 deletions fuzzy/fuzzy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package fuzzy

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestSearchFuzzy(t *testing.T) {
cases := []struct {
source string
targets []string
expected []string
}{
{
"cert",
[]string{"docker bash: docker exec -ti container bash", "openssl view cert: openssl x509 -text -noout -in"},
[]string{"openssl view cert: openssl x509 -text -noout -in", "docker bash: docker exec -ti container bash"},
},
{
"",
[]string{"banana", "apple", "pear"},
[]string{"banana", "apple", "pear"},
},
}

for _, c := range cases {
ranked := SearchFuzzy(c.source, c.targets)
var actual []string
for _, r := range ranked {
actual = append(actual, r.Str)
}
assert.Equal(t, c.expected, actual)
}
}
Loading

0 comments on commit c6dd079

Please sign in to comment.