From 190e1e4d116b89f2e671510143c7ccaa25e9d586 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Mon, 3 Aug 2015 18:14:08 +0200 Subject: [PATCH 1/2] Sorting cache search by Levenshtein distance (Fix #87) --- README.md | 1 + api/api.go | 12 +++--- api/cache.go | 105 +++++++++++++++++++++++++++++++++++-------------- api/helpers.go | 7 +++- 4 files changed, 89 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 987574a0da..7eb8090cba 100644 --- a/README.md +++ b/README.md @@ -1045,6 +1045,7 @@ $ scw inspect myserver | jq '.[0].public_ip.address' * Support of `scw _flush-cache` internal command * `scw run --gateway ...` or `SCW_GATEWAY="..." scw run ...` now creates a server without public ip address ([#74](https://github.com/scaleway/scaleway-cli/issues/74)) * `scw inspect TYPE:xxx TYPE:yyy` will only refresh cache for `TYPE` +* Sorting cache search by Levenshtein distance ([#87](https://github.com/scaleway/scaleway-cli/issues/87)) #### Fixes diff --git a/api/api.go b/api/api.go index adee6f0f2c..84ebf8589e 100644 --- a/api/api.go +++ b/api/api.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" "os" + "sort" "strings" "text/tabwriter" "text/template" @@ -943,7 +944,7 @@ func (s *ScalewayAPI) PutVolume(volumeID string, definition ScalewayVolumePutDef } // ResolveServer attempts the find a matching Identifier for the input string -func (s *ScalewayAPI) ResolveServer(needle string) ([]ScalewayResolverResult, error) { +func (s *ScalewayAPI) ResolveServer(needle string) (ScalewayResolverResults, error) { servers := s.Cache.LookUpServers(needle, true) if len(servers) == 0 { _, err := s.GetServers(true, 0) @@ -956,7 +957,7 @@ func (s *ScalewayAPI) ResolveServer(needle string) ([]ScalewayResolverResult, er } // ResolveSnapshot attempts the find a matching Identifier for the input string -func (s *ScalewayAPI) ResolveSnapshot(needle string) ([]ScalewayResolverResult, error) { +func (s *ScalewayAPI) ResolveSnapshot(needle string) (ScalewayResolverResults, error) { snapshots := s.Cache.LookUpSnapshots(needle, true) if len(snapshots) == 0 { _, err := s.GetSnapshots() @@ -969,7 +970,7 @@ func (s *ScalewayAPI) ResolveSnapshot(needle string) ([]ScalewayResolverResult, } // ResolveImage attempts the find a matching Identifier for the input string -func (s *ScalewayAPI) ResolveImage(needle string) ([]ScalewayResolverResult, error) { +func (s *ScalewayAPI) ResolveImage(needle string) (ScalewayResolverResults, error) { images := s.Cache.LookUpImages(needle, true) if len(images) == 0 { _, err := s.GetImages() @@ -982,7 +983,7 @@ func (s *ScalewayAPI) ResolveImage(needle string) ([]ScalewayResolverResult, err } // ResolveBootscript attempts the find a matching Identifier for the input string -func (s *ScalewayAPI) ResolveBootscript(needle string) ([]ScalewayResolverResult, error) { +func (s *ScalewayAPI) ResolveBootscript(needle string) (ScalewayResolverResults, error) { bootscripts := s.Cache.LookUpBootscripts(needle, true) if len(bootscripts) == 0 { _, err := s.GetBootscripts() @@ -1225,11 +1226,12 @@ func (s *ScalewayAPI) GetServerID(needle string) string { return "" } -func showResolverResults(needle string, results []ScalewayResolverResult) error { +func showResolverResults(needle string, results ScalewayResolverResults) error { log.Errorf("Too many candidates for %s (%d)", needle, len(results)) w := tabwriter.NewWriter(os.Stderr, 20, 1, 3, ' ', 0) defer w.Flush() + sort.Sort(results) for _, result := range results { fmt.Fprintf(w, "- %s\t%s\t%s\n", result.TruncIdentifier(), result.CodeName(), result.Name) } diff --git a/api/cache.go b/api/cache.go index c754c2fde7..799c056ad8 100644 --- a/api/cache.go +++ b/api/cache.go @@ -14,6 +14,7 @@ import ( "strings" "sync" + "github.com/renstrom/fuzzysearch/fuzzy" "github.com/scaleway/scaleway-cli/vendor/code.google.com/p/go-uuid/uuid" ) @@ -65,6 +66,22 @@ type ScalewayResolverResult struct { Identifier string Type int Name string + Needle string + RankMatch int +} + +type ScalewayResolverResults []ScalewayResolverResult + +func (s ScalewayResolverResults) Len() int { + return len(s) +} + +func (s ScalewayResolverResults) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ScalewayResolverResults) Less(i, j int) bool { + return s[i].RankMatch < s[j].RankMatch } // TruncIdentifier returns first 8 characters of an Identifier (UUID) @@ -174,13 +191,18 @@ func (c *ScalewayCache) Save() error { return nil } +func (s *ScalewayResolverResult) ComputeRankMatch(needle string) { + s.Needle = needle + s.RankMatch = fuzzy.RankMatch(needle, s.Name) +} + // LookUpImages attempts to return identifiers matching a pattern -func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) []ScalewayResolverResult { +func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) ScalewayResolverResults { c.Lock.Lock() defer c.Lock.Unlock() - var res []ScalewayResolverResult - var exactMatches []ScalewayResolverResult + var res ScalewayResolverResults + var exactMatches ScalewayResolverResults if acceptUUID && uuid.Parse(needle) != nil { entry := ScalewayResolverResult{ @@ -188,6 +210,7 @@ func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) []ScalewayR Name: needle, Type: IdentifierImage, } + entry.ComputeRankMatch(needle) res = append(res, entry) } @@ -201,6 +224,7 @@ func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) []ScalewayR Name: name, Type: IdentifierImage, } + entry.ComputeRankMatch(needle) exactMatches = append(exactMatches, entry) } if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(name) { @@ -209,6 +233,7 @@ func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) []ScalewayR Name: name, Type: IdentifierImage, } + entry.ComputeRankMatch(needle) res = append(res, entry) } } @@ -221,12 +246,12 @@ func (c *ScalewayCache) LookUpImages(needle string, acceptUUID bool) []ScalewayR } // LookUpSnapshots attempts to return identifiers matching a pattern -func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) []ScalewayResolverResult { +func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) ScalewayResolverResults { c.Lock.Lock() defer c.Lock.Unlock() - var res []ScalewayResolverResult - var exactMatches []ScalewayResolverResult + var res ScalewayResolverResults + var exactMatches ScalewayResolverResults if acceptUUID && uuid.Parse(needle) != nil { entry := ScalewayResolverResult{ @@ -234,6 +259,7 @@ func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) []Scalew Name: needle, Type: IdentifierSnapshot, } + entry.ComputeRankMatch(needle) res = append(res, entry) } @@ -246,6 +272,7 @@ func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) []Scalew Name: name, Type: IdentifierSnapshot, } + entry.ComputeRankMatch(needle) exactMatches = append(exactMatches, entry) } if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(name) { @@ -254,6 +281,7 @@ func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) []Scalew Name: name, Type: IdentifierSnapshot, } + entry.ComputeRankMatch(needle) res = append(res, entry) } } @@ -266,12 +294,12 @@ func (c *ScalewayCache) LookUpSnapshots(needle string, acceptUUID bool) []Scalew } // LookUpVolumes attempts to return identifiers matching a pattern -func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) []ScalewayResolverResult { +func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) ScalewayResolverResults { c.Lock.Lock() defer c.Lock.Unlock() - var res []ScalewayResolverResult - var exactMatches []ScalewayResolverResult + var res ScalewayResolverResults + var exactMatches ScalewayResolverResults if acceptUUID && uuid.Parse(needle) != nil { entry := ScalewayResolverResult{ @@ -279,6 +307,7 @@ func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) []Scaleway Name: needle, Type: IdentifierVolume, } + entry.ComputeRankMatch(needle) res = append(res, entry) } @@ -290,6 +319,7 @@ func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) []Scaleway Name: name, Type: IdentifierVolume, } + entry.ComputeRankMatch(needle) exactMatches = append(exactMatches, entry) } if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(name) { @@ -298,6 +328,7 @@ func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) []Scaleway Name: name, Type: IdentifierVolume, } + entry.ComputeRankMatch(needle) res = append(res, entry) } } @@ -310,12 +341,12 @@ func (c *ScalewayCache) LookUpVolumes(needle string, acceptUUID bool) []Scaleway } // LookUpBootscripts attempts to return identifiers matching a pattern -func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) []ScalewayResolverResult { +func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) ScalewayResolverResults { c.Lock.Lock() defer c.Lock.Unlock() - var res []ScalewayResolverResult - var exactMatches []ScalewayResolverResult + var res ScalewayResolverResults + var exactMatches ScalewayResolverResults if acceptUUID && uuid.Parse(needle) != nil { entry := ScalewayResolverResult{ @@ -323,6 +354,7 @@ func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) []Scal Name: needle, Type: IdentifierBootscript, } + entry.ComputeRankMatch(needle) res = append(res, entry) } @@ -334,6 +366,7 @@ func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) []Scal Name: name, Type: IdentifierBootscript, } + entry.ComputeRankMatch(needle) exactMatches = append(exactMatches, entry) } if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(name) { @@ -342,6 +375,7 @@ func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) []Scal Name: name, Type: IdentifierBootscript, } + entry.ComputeRankMatch(needle) res = append(res, entry) } } @@ -354,12 +388,12 @@ func (c *ScalewayCache) LookUpBootscripts(needle string, acceptUUID bool) []Scal } // LookUpServers attempts to return identifiers matching a pattern -func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) []ScalewayResolverResult { +func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) ScalewayResolverResults { c.Lock.Lock() defer c.Lock.Unlock() - var res []ScalewayResolverResult - var exactMatches []ScalewayResolverResult + var res ScalewayResolverResults + var exactMatches ScalewayResolverResults if acceptUUID && uuid.Parse(needle) != nil { entry := ScalewayResolverResult{ @@ -367,6 +401,7 @@ func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) []Scaleway Name: needle, Type: IdentifierServer, } + entry.ComputeRankMatch(needle) res = append(res, entry) } @@ -378,6 +413,7 @@ func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) []Scaleway Name: name, Type: IdentifierServer, } + entry.ComputeRankMatch(needle) exactMatches = append(exactMatches, entry) } if strings.HasPrefix(identifier, needle) || nameRegex.MatchString(name) { @@ -386,6 +422,7 @@ func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) []Scaleway Name: name, Type: IdentifierServer, } + entry.ComputeRankMatch(needle) res = append(res, entry) } } @@ -398,7 +435,7 @@ func (c *ScalewayCache) LookUpServers(needle string, acceptUUID bool) []Scaleway } // removeDuplicatesResults transforms an array into a unique array -func removeDuplicatesResults(elements []ScalewayResolverResult) []ScalewayResolverResult { +func removeDuplicatesResults(elements ScalewayResolverResults) ScalewayResolverResults { encountered := map[string]ScalewayResolverResult{} // Create a map of all unique elements. @@ -407,7 +444,7 @@ func removeDuplicatesResults(elements []ScalewayResolverResult) []ScalewayResolv } // Place all keys from the map into a slice. - results := []ScalewayResolverResult{} + results := ScalewayResolverResults{} for _, result := range encountered { results = append(results, result) } @@ -439,58 +476,68 @@ func parseNeedle(input string) (identifierType int, needle string) { } // LookUpIdentifiers attempts to return identifiers matching a pattern -func (c *ScalewayCache) LookUpIdentifiers(needle string) []ScalewayResolverResult { - results := []ScalewayResolverResult{} +func (c *ScalewayCache) LookUpIdentifiers(needle string) ScalewayResolverResults { + results := ScalewayResolverResults{} identifierType, needle := parseNeedle(needle) if identifierType&(IdentifierUnknown|IdentifierServer) > 0 { for _, result := range c.LookUpServers(needle, false) { - results = append(results, ScalewayResolverResult{ + entry := ScalewayResolverResult{ Identifier: result.Identifier, Name: result.Name, Type: IdentifierServer, - }) + } + entry.ComputeRankMatch(needle) + results = append(results, entry) } } if identifierType&(IdentifierUnknown|IdentifierImage) > 0 { for _, result := range c.LookUpImages(needle, false) { - results = append(results, ScalewayResolverResult{ + entry := ScalewayResolverResult{ Identifier: result.Identifier, Name: result.Name, Type: IdentifierImage, - }) + } + entry.ComputeRankMatch(needle) + results = append(results, entry) } } if identifierType&(IdentifierUnknown|IdentifierSnapshot) > 0 { for _, result := range c.LookUpSnapshots(needle, false) { - results = append(results, ScalewayResolverResult{ + entry := ScalewayResolverResult{ Identifier: result.Identifier, Name: result.Name, Type: IdentifierSnapshot, - }) + } + entry.ComputeRankMatch(needle) + results = append(results, entry) } } if identifierType&(IdentifierUnknown|IdentifierVolume) > 0 { for _, result := range c.LookUpVolumes(needle, false) { - results = append(results, ScalewayResolverResult{ + entry := ScalewayResolverResult{ Identifier: result.Identifier, Name: result.Name, Type: IdentifierVolume, - }) + } + entry.ComputeRankMatch(needle) + results = append(results, entry) } } if identifierType&(IdentifierUnknown|IdentifierBootscript) > 0 { for _, result := range c.LookUpBootscripts(needle, false) { - results = append(results, ScalewayResolverResult{ + entry := ScalewayResolverResult{ Identifier: result.Identifier, Name: result.Name, Type: IdentifierBootscript, - }) + } + entry.ComputeRankMatch(needle) + results = append(results, entry) } } diff --git a/api/helpers.go b/api/helpers.go index ead085f35d..e13217e771 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -7,6 +7,7 @@ package api import ( "fmt" "os" + "sort" "strings" "sync" "time" @@ -20,7 +21,7 @@ import ( // ScalewayResolvedIdentifier represents a list of matching identifier for a specifier pattern type ScalewayResolvedIdentifier struct { // Identifiers holds matching identifiers - Identifiers []ScalewayResolverResult + Identifiers ScalewayResolverResults // Needle is the criteria used to lookup identifiers Needle string @@ -137,6 +138,8 @@ func GetIdentifier(api *ScalewayAPI, needle string) *ScalewayResolverResult { log.Fatalf("No such identifier: %s", needle) } log.Errorf("Too many candidates for %s (%d)", needle, len(idents)) + + sort.Sort(idents) for _, identifier := range idents { // FIXME: also print the name fmt.Fprint(os.Stderr, "- %s\n", identifier.Identifier) @@ -146,7 +149,7 @@ func GetIdentifier(api *ScalewayAPI, needle string) *ScalewayResolverResult { } // ResolveIdentifier resolves needle provided by the user -func ResolveIdentifier(api *ScalewayAPI, needle string) []ScalewayResolverResult { +func ResolveIdentifier(api *ScalewayAPI, needle string) ScalewayResolverResults { idents := api.Cache.LookUpIdentifiers(needle) if len(idents) > 0 { return idents From f117d3ffc0d7fc1b100ae40aeab72905a2945ad9 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Mon, 3 Aug 2015 18:19:52 +0200 Subject: [PATCH 2/2] party -c -d=vendor --- api/cache.go | 2 +- .../renstrom/fuzzysearch/fuzzy/fuzzy.go | 93 +++++++++++++ .../renstrom/fuzzysearch/fuzzy/fuzzy_test.go | 127 ++++++++++++++++++ .../renstrom/fuzzysearch/fuzzy/levenshtein.go | 43 ++++++ .../fuzzysearch/fuzzy/levenshtein_test.go | 36 +++++ 5 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go create mode 100644 vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy_test.go create mode 100644 vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go create mode 100644 vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein_test.go diff --git a/api/cache.go b/api/cache.go index 799c056ad8..2d7176fa84 100644 --- a/api/cache.go +++ b/api/cache.go @@ -14,8 +14,8 @@ import ( "strings" "sync" - "github.com/renstrom/fuzzysearch/fuzzy" "github.com/scaleway/scaleway-cli/vendor/code.google.com/p/go-uuid/uuid" + "github.com/scaleway/scaleway-cli/vendor/github.com/renstrom/fuzzysearch/fuzzy" ) // ScalewayCache is used not to query the API to resolve full identifiers diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go new file mode 100644 index 0000000000..fbc3d65ccd --- /dev/null +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy.go @@ -0,0 +1,93 @@ +// Fuzzy searching allows for flexibly matching a string with partial input, +// useful for filtering data very quickly based on lightweight user input. +package fuzzy + +import "unicode/utf8" + +// Match returns true if source matches target using a fuzzy-searching +// algorithm. Note that it doesn't implement Levenshtein distance (see +// RankMatch instead), but rather a simplified version where there's no +// approximation. The method will return true only if each character in the +// source can be found in the target and occurs after the preceding matches. +func Match(source, target string) bool { + if len(source) > len(target) { + return false + } + + if len(source) == len(target) { + return source == target + } +Outer: + for _, r1 := range source { + for i, r2 := range target { + if r1 == r2 { + target = target[i+utf8.RuneLen(r2):] + continue Outer + } + } + return false + } + + return true +} + +// Find will return a list of strings in targets that fuzzy matches source. +func Find(source string, targets []string) []string { + var matches []string + + for _, target := range targets { + if Match(source, target) { + matches = append(matches, target) + } + } + + return matches +} + +// RankMatch is similar to Match except it will measure the Levenshtein +// distance between the source and the target and return its result. If there +// was no match, it will return -1. +func RankMatch(source, target string) int { + match := Match(source, target) + if !match { + return -1 + } + return LevenshteinDistance(source, target) +} + +// RankFind is similar to Find, except it will also rank all matches using +// Levenshtein distance. +func RankFind(source string, targets []string) ranks { + var r ranks + for _, target := range Find(source, targets) { + r = append(r, Rank{ + Source: source, + Target: target, + Distance: LevenshteinDistance(source, target), + }) + } + return r +} + +type Rank struct { + // Source is used as the source for matching. + Source string + // Target is the word matched against. + Target string + // Distance is the Levenshtein distance between Source and Target. + Distance int +} + +type ranks []Rank + +func (r ranks) Len() int { + return len(r) +} + +func (r ranks) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r ranks) Less(i, j int) bool { + return r[i].Distance < r[j].Distance +} diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy_test.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy_test.go new file mode 100644 index 0000000000..a074fa5c09 --- /dev/null +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/fuzzy_test.go @@ -0,0 +1,127 @@ +package fuzzy + +import ( + "fmt" + "sort" + "testing" +) + +var fuzzyTests = []struct { + source string + target string + wanted bool + rank int +}{ + {"twl", "cartwheel", true, 6}, + {"cart", "cartwheel", true, 5}, + {"cw", "cartwheel", true, 7}, + {"ee", "cartwheel", true, 7}, + {"art", "cartwheel", true, 6}, + {"eeel", "cartwheel", false, -1}, + {"dog", "cartwheel", false, -1}, + {"ёлка", "ёлочка", true, 2}, + {"ветер", "ёлочка", false, -1}, + {"中国", "中华人民共和国", true, 5}, + {"日本", "中华人民共和国", false, -1}, +} + +func TestFuzzyMatch(t *testing.T) { + for _, val := range fuzzyTests { + match := Match(val.source, val.target) + if match != val.wanted { + t.Errorf("%s in %s expected match to be %t, got %t", + val.source, val.target, val.wanted, match) + } + } +} + +func TestFuzzyFind(t *testing.T) { + target := []string{"cartwheel", "foobar", "wheel", "baz"} + wanted := []string{"cartwheel", "wheel"} + + matches := Find("whl", target) + + if len(matches) != len(wanted) { + t.Errorf("expected %s, got %s", wanted, matches) + } + + for i := range wanted { + if wanted[i] != matches[i] { + t.Errorf("expected %s, got %s", wanted, matches) + } + } +} + +func TestRankMatch(t *testing.T) { + for _, val := range fuzzyTests { + rank := RankMatch(val.source, val.target) + if rank != val.rank { + t.Errorf("expected ranking %d, got %d for %s in %s", val.rank, rank, val.source, val.target) + } + } +} + +func TestRankFind(t *testing.T) { + target := []string{"cartwheel", "foobar", "wheel", "baz"} + wanted := []Rank{ + {"whl", "cartwheel", 6}, + {"whl", "wheel", 2}, + } + + ranks := RankFind("whl", target) + + if len(ranks) != len(wanted) { + t.Errorf("expected %+v, got %+v", wanted, ranks) + } + + for i := range wanted { + if wanted[i] != ranks[i] { + t.Errorf("expected %+v, got %+v", wanted, ranks) + } + } +} + +func TestSortingRanks(t *testing.T) { + rs := ranks{{"a", "b", 1}, {"a", "cc", 2}, {"a", "a", 0}} + wanted := ranks{rs[2], rs[0], rs[1]} + + sort.Sort(rs) + + for i := range wanted { + if wanted[i] != rs[i] { + t.Errorf("expected %+v, got %+v", wanted, rs) + } + } +} + +func BenchmarkMatch(b *testing.B) { + for i := 0; i < b.N; i++ { + Match("kitten", "sitting") + } +} + +func BenchmarkRankMatch(b *testing.B) { + for i := 0; i < b.N; i++ { + RankMatch("kitten", "sitting") + } +} + +func ExampleMatch() { + fmt.Print(Match("twl", "cartwheel")) + // Output: true +} + +func ExampleFind() { + fmt.Print(Find("whl", []string{"cartwheel", "foobar", "wheel", "baz"})) + // Output: [cartwheel wheel] +} + +func ExampleRankMatch() { + fmt.Print(RankMatch("twl", "cartwheel")) + // Output: 6 +} + +func ExampleRankFind() { + fmt.Printf("%+v", RankFind("whl", []string{"cartwheel", "foobar", "wheel", "baz"})) + // Output: [{Source:whl Target:cartwheel Distance:6} {Source:whl Target:wheel Distance:2}] +} diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go new file mode 100644 index 0000000000..237923d345 --- /dev/null +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein.go @@ -0,0 +1,43 @@ +package fuzzy + +// LevenshteinDistance measures the difference between two strings. +// The Levenshtein distance between two words is the minimum number of +// single-character edits (i.e. insertions, deletions or substitutions) +// required to change one word into the other. +// +// This implemention is optimized to use O(min(m,n)) space and is based on the +// optimized C version found here: +// http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#C +func LevenshteinDistance(s, t string) int { + r1, r2 := []rune(s), []rune(t) + column := make([]int, len(r1)+1) + + for y := 1; y <= len(r1); y++ { + column[y] = y + } + + for x := 1; x <= len(r2); x++ { + column[0] = x + + for y, lastDiag := 1, x-1; y <= len(r1); y++ { + oldDiag := column[y] + cost := 0 + if r1[y-1] != r2[x-1] { + cost = 1 + } + column[y] = min(column[y]+1, column[y-1]+1, lastDiag+cost) + lastDiag = oldDiag + } + } + + return column[len(r1)] +} + +func min(a, b, c int) int { + if a < b && a < c { + return a + } else if b < c { + return b + } + return c +} diff --git a/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein_test.go b/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein_test.go new file mode 100644 index 0000000000..2dc673c8b7 --- /dev/null +++ b/vendor/github.com/renstrom/fuzzysearch/fuzzy/levenshtein_test.go @@ -0,0 +1,36 @@ +package fuzzy + +import "testing" + +var levenshteinDistanceTests = []struct { + s, t string + wanted int +}{ + {"a", "a", 0}, + {"ab", "ab", 0}, + {"ab", "aa", 1}, + {"ab", "aa", 1}, + {"ab", "aaa", 2}, + {"bbb", "a", 3}, + {"kitten", "sitting", 3}, + {"ёлка", "ёлочка", 2}, + {"ветер", "ёлочка", 6}, + {"中国", "中华人民共和国", 5}, + {"日本", "中华人民共和国", 7}, +} + +func TestLevenshtein(t *testing.T) { + for _, test := range levenshteinDistanceTests { + distance := LevenshteinDistance(test.s, test.t) + if distance != test.wanted { + t.Errorf("got distance %d, expected %d for %s in %s", distance, test.wanted, test.s, test.t) + } + } +} + +func BenchmarkLevenshteinDistance(b *testing.B) { + for i := 0; i < b.N; i++ { + LevenshteinDistance("aaa", "aba") + LevenshteinDistance("kitten", "sitting") + } +}