diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..c754555
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,85 @@
+modules-download-mode: readonly
+
+linters:
+
+ enable:
+ - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
+ - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false]
+ - cyclop # checks function and package cyclomatic complexity
+ - deadcode # Finds unused code [fast: false, auto-fix: false]
+ - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: false, auto-fix: false]
+ - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
+ - dupl # Tool for code clone detection [fast: true, auto-fix: false]
+ - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
+ - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
+ - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false]
+ - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
+ - exhaustive # checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
+ - exhaustivestruct # Checks if all struct's fields are initialized
+ - exportloopref # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
+ - forbidigo # we want to use some fmt.Println. Forbids identifiers
+ - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
+ - funlen # Tool for detection of long functions [fast: true, auto-fix: false]
+ - gci # mix the std and tealticks imports. Gci control golang package import order and make it always deterministic. [fast: true, auto-fix: true]
+ - gochecknoinits # we want to use init(). gochecknoinits checks that no init functions are present in Go code
+ - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
+ - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
+ - gocritic # Provides many diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false]
+ - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
+ - godot # Check if comments end in a period [fast: true, auto-fix: true]
+ - goerr113 # Golang linter to check the errors handling expressions
+ - gofmt # goimports does more. Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
+ - gofumpt # mix the std and tealticks imports. Gofumpt checks whether code was gofumpt-ed.
+ - goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]
+ - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
+ - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
+ - gomnd # hard coded values in source code
+ - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false]
+ - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false]
+ - goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false]
+ - gosec # gas: Inspects source code for security problems [fast: false, auto-fix: false]
+ - gosimple # megacheck: Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false]
+ - govet # vet, vetshadow: Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
+ - ifshort # Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false]
+ - importas # Enforces consistent import aliases [fast: false, auto-fix: false]
+ - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
+ - lll # Reports long lines [fast: true, auto-fix: false]
+ - makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
+ - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
+ - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
+ - nestif # Reports deeply nested if statements [fast: true, auto-fix: false]
+ - nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
+ - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false]
+ - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
+ - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false]
+ - predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false]
+ - promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false]
+ - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
+ - rowserrcheck # checks whether Err of rows is checked successfully [fast: false, auto-fix: false]
+ - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false]
+ - staticcheck # megacheck: Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false]
+ - structcheck # Finds unused struct fields [fast: false, auto-fix: false]
+ - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false]
+ - tagliatelle # Checks the struct tags. [fast: true, auto-fix: false]
+ - testpackage # we want package name `ref_test` instead of `ref`. testpackage wants separate _test package
+ - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers
+ - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false]
+ - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false]
+ - unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false]
+ - unparam # Reports unused function parameters [fast: false, auto-fix: false]
+ - unused # megacheck: Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
+ - varcheck # Finds unused global variables and constants [fast: false, auto-fix: false]
+ - wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
+ - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true]
+ - wrapcheck # Checks that errors returned from external packages are wrapped
+
+ disable:
+ - gochecknoglobals # we simply use global vars. gochecknoglobals checks that no global variables exist
+ - godox # we know our code contains TODO / FIXME. godox detects FIXME, TODO and other comment keywords
+ - golint # Replaced by revive. Gofmt reformats Go source code, whereas golint prints out style mistakes
+ - interfacer # Replaced by exportloopref. Suggest narrower interface types
+ - maligned # Replaced by govet 'fieldalignment' (detect Go structs that would take less memory if their fields were sorted)
+ - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false]
+ - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test
+ - scopelint # Replaced by exportloopref. scopelint checks for unpinned variables in go programs
+ - wsl # Whitespace Linter - Forces you to use empty lines!
diff --git a/.travis.yml b/.travis.yml
index 6756d80..f77acde 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,6 @@
+arch:
+ - amd64
+ - ppc64le
language: go
go:
- 1.x
diff --git a/Gopkg.lock b/Gopkg.lock
deleted file mode 100644
index 6e3a7fe..0000000
--- a/Gopkg.lock
+++ /dev/null
@@ -1,20 +0,0 @@
-# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
-
-
-[[projects]]
- branch = "master"
- digest = "1:ee97ec8a00b2424570c1ce53d7b410e96fbd4c241b29df134276ff6aa3750335"
- name = "github.com/kylelemons/godebug"
- packages = [
- "diff",
- "pretty",
- ]
- pruneopts = ""
- revision = "d65d576e9348f5982d7f6d83682b694e731a45c6"
-
-[solve-meta]
- analyzer-name = "dep"
- analyzer-version = 1
- input-imports = ["github.com/kylelemons/godebug/pretty"]
- solver-name = "gps-cdcl"
- solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
deleted file mode 100644
index 8f96b11..0000000
--- a/Gopkg.toml
+++ /dev/null
@@ -1,4 +0,0 @@
-# Test dependency
-[[constraint]]
- branch = "master"
- name = "github.com/kylelemons/godebug"
diff --git a/LICENSE b/LICENSE
index f848719..5b67417 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
The MIT License (MIT)
-Copyright (c) 2017 Sahil Muthoo
+Copyright (c) 2017-2021 Sahil Muthoo and some other contributors
+Copyright (c) 2021 Teal.Finance contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 7fa2be4..0000000
--- a/Makefile
+++ /dev/null
@@ -1,57 +0,0 @@
-.PHONY: all
-all: setup lint test
-
-.PHONY: test
-test: setup
- go test -bench ./...
-
-.PHONY: cover
-cover: setup
- mkdir -p coverage
- gocov test ./... | gocov-html > coverage/coverage.html
-
-sources = $(shell find . -name '*.go' -not -path './vendor/*')
-.PHONY: goimports
-goimports: setup
- goimports -w $(sources)
-
-.PHONY: lint
-lint: setup
- gometalinter ./... --enable=goimports --disable=gocyclo --vendor -t
-
-.PHONY: install
-install: setup
- go install
-
-BIN_DIR := $(GOPATH)/bin
-GOIMPORTS := $(BIN_DIR)/goimports
-GOMETALINTER := $(BIN_DIR)/gometalinter
-DEP := $(BIN_DIR)/dep
-GOCOV := $(BIN_DIR)/gocov
-GOCOV_HTML := $(BIN_DIR)/gocov-html
-
-$(GOIMPORTS):
- go get -u golang.org/x/tools/cmd/goimports
-
-$(GOMETALINTER):
- go get -u github.com/alecthomas/gometalinter
- gometalinter --install &> /dev/null
-
-$(GOCOV):
- go get -u github.com/axw/gocov/gocov
-
-$(GOCOV_HTML):
- go get -u gopkg.in/matm/v1/gocov-html
-
-$(DEP):
- go get -u github.com/golang/dep/cmd/dep
-
-tools: $(GOIMPORTS) $(GOMETALINTER) $(GOCOV) $(GOCOV_HTML) $(DEP)
-
-vendor: $(DEP)
- dep ensure
-
-setup: tools vendor
-
-updatedeps:
- dep ensure -update
diff --git a/README.md b/README.md
index ea7bf22..66be302 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,26 @@
# fuzzy
-[](https://travis-ci.org/sahilm/fuzzy)
-[](https://godoc.org/github.com/sahilm/fuzzy)
-Go library that provides fuzzy string matching optimized for filenames and code symbols in the style of Sublime Text,
+[](https://travis-ci.org/teal-finance/fuzzy)
+[](https://godoc.org/github.com/teal-finance/fuzzy)
+
+Go library that provides fuzzy string matching optimized for filenames and code symbols in the style of Sublime Text,
VSCode, IntelliJ IDEA et al. This library is external dependency-free. It only depends on the Go standard library.
## Features
-- Intuitive matching. Results are returned in descending order of match quality. Quality is determined by:
+- Intuitive matching. Quality is determined by:
- The first character in the pattern matches the first character in the match string.
- The matched character is camel cased.
- The matched character follows a separator such as an underscore character.
- The matched character is adjacent to a previous match.
+ - Favor case sensitive matching: if two similar targets mathes, the one respecting the input case has a higher score.
+ - Insensitive to the punctuations `/-_ .\` thus "BTC/USD" matches "BTC-USD".
+
+- `Find()` and `FindFrom()` return result in descending order of match quality.
+
+- `BestMatch()` and `BestMatchFrom()` return the matching having the highest score.
- Speed. Matches are returned in milliseconds. It's perfect for interactive search boxes.
@@ -23,14 +30,14 @@ VSCode, IntelliJ IDEA et al. This library is external dependency-free. It only d
## Demo
-Here is a [demo](_example/main.go) of matching various patterns against ~16K files from the Unreal Engine 4 codebase.
+Here is a [demo](example/main.go) of matching various patterns against ~16K files from the Unreal Engine 4 codebase.

-You can run the demo yourself like so:
+Run the demo:
```
-cd _example/
+cd example
go get github.com/jroimartin/gocui
go run main.go
```
@@ -43,39 +50,40 @@ The following example prints out matches with the matched chars in bold.
package main
import (
- "fmt"
+ "fmt"
- "github.com/sahilm/fuzzy"
+ "github.com/teal-finance/fuzzy"
)
func main() {
- const bold = "\033[1m%s\033[0m"
- pattern := "mnr"
- data := []string{"game.cpp", "moduleNameResolver.ts", "my name is_Ramsey"}
-
- matches := fuzzy.Find(pattern, data)
-
- for _, match := range matches {
- for i := 0; i < len(match.Str); i++ {
- if contains(i, match.MatchedIndexes) {
- fmt.Print(fmt.Sprintf(bold, string(match.Str[i])))
- } else {
- fmt.Print(string(match.Str[i]))
- }
- }
- fmt.Println()
- }
+ const bold = "\033[1m%s\033[0m"
+ pattern := "mnr"
+ data := []string{"game.cpp", "moduleNameResolver.ts", "my name is_Ramsey"}
+
+ matches := fuzzy.Find(pattern, data)
+
+ for _, match := range matches {
+ for i := 0; i < len(match.Str); i++ {
+ if contains(i, match.MatchedIndexes) {
+ fmt.Print(fmt.Sprintf(bold, string(match.Str[i])))
+ } else {
+ fmt.Print(string(match.Str[i]))
+ }
+ }
+ fmt.Println()
+ }
}
func contains(needle int, haystack []int) bool {
- for _, i := range haystack {
- if needle == i {
- return true
- }
- }
- return false
+ for _, i := range haystack {
+ if needle == i {
+ return true
+ }
+ }
+ return false
}
-```
+```
+
If the data you want to match isn't a slice of strings, you can use `FindFrom` by implementing
the provided `Source` interface. Here's an example:
@@ -83,88 +91,154 @@ the provided `Source` interface. Here's an example:
package main
import (
- "fmt"
+ "fmt"
- "github.com/sahilm/fuzzy"
+ "github.com/teal-finance/fuzzy"
)
type employee struct {
- name string
- age int
+ name string
+ age int
}
type employees []employee
func (e employees) String(i int) string {
- return e[i].name
+ return e[i].name
}
func (e employees) Len() int {
- return len(e)
+ return len(e)
}
func main() {
- emps := employees{
- {
- name: "Alice",
- age: 45,
- },
- {
- name: "Bob",
- age: 35,
- },
- {
- name: "Allie",
- age: 35,
- },
- }
- results := fuzzy.FindFrom("al", emps)
- for _, r := range results {
- fmt.Println(emps[r.Index])
- }
+ emps := employees{
+ {
+ name: "Alice",
+ age: 45,
+ },
+ {
+ name: "Bob",
+ age: 35,
+ },
+ {
+ name: "Allie",
+ age: 35,
+ },
+ }
+ results := fuzzy.FindFrom("al", emps)
+ for _, r := range results {
+ fmt.Println(emps[r.Index])
+ }
}
```
-Check out the [godoc](https://godoc.org/github.com/sahilm/fuzzy) for detailed documentation.
+Check out the [godoc](https://pkg.go.dev/github.com/teal-finance/fuzzy) for detailed documentation.
## Installation
-`go get github.com/sahilm/fuzzy` or use your favorite dependency management tool.
+`go get github.com/teal-finance/fuzzy`
## Speed
-Here are a few benchmark results on a normal laptop.
+The benchmark includes:
+1. the [forked project](https://github.com/isacikgoz/fuzzy) by @isacikgoz using Go channel (⚠️ the channel overhead slows down this bench),
+2. the [original project](https://github.com/sahilm/fuzzy) from @sahilm,
+3. the current repo, which is twice as fast as the original,
+4. the memory-optimized `BestMatch()`, 25% faster than `Find()`.
```
-BenchmarkFind/with_unreal_4_(~16K_files)-4 100 12915315 ns/op
-BenchmarkFind/with_linux_kernel_(~60K_files)-4 50 30885038 ns/op
+$ go test -count 6 -benchmem -run=^$ -bench . github.com/teal-finance/fuzzy
+
+goos: linux
+goarch: amd64
+pkg: github.com/teal-finance/fuzzy
+cpu: AMD Ryzen 9 3900X 12-Core Processor
+
+BenchmarkUnrealFiles/isacikgoz.Find-24 86 13483431 ns/op 151874 B/op 898 allocs/op
+BenchmarkUnrealFiles/isacikgoz.Find-24 87 13620413 ns/op 151875 B/op 898 allocs/op
+BenchmarkUnrealFiles/isacikgoz.Find-24 90 13537883 ns/op 151873 B/op 898 allocs/op
+BenchmarkUnrealFiles/isacikgoz.Find-24 90 13608595 ns/op 151864 B/op 898 allocs/op
+BenchmarkUnrealFiles/isacikgoz.Find-24 93 13468849 ns/op 151872 B/op 898 allocs/op
+BenchmarkUnrealFiles/isacikgoz.Find-24 100 13583070 ns/op 151875 B/op 898 allocs/op
+
+BenchmarkUnrealFiles/sahilm.Find-24 139 8147677 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/sahilm.Find-24 140 7849173 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/sahilm.Find-24 148 7483526 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/sahilm.Find-24 150 7708139 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/sahilm.Find-24 156 8012760 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/sahilm.Find-24 157 7671143 ns/op 151752 B/op 896 allocs/op
+
+BenchmarkUnrealFiles/teal.Find-24 256 4403617 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/teal.Find-24 258 4568313 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/teal.Find-24 282 4592112 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/teal.Find-24 286 4675680 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/teal.Find-24 314 4102624 ns/op 151752 B/op 896 allocs/op
+BenchmarkUnrealFiles/teal.Find-24 324 4030270 ns/op 151752 B/op 896 allocs/op
+
+BenchmarkUnrealFiles/teal.BestMatch-24 374 2683358 ns/op 200 B/op 5 allocs/op
+BenchmarkUnrealFiles/teal.BestMatch-24 376 2748506 ns/op 200 B/op 5 allocs/op
+BenchmarkUnrealFiles/teal.BestMatch-24 381 2807797 ns/op 200 B/op 5 allocs/op
+BenchmarkUnrealFiles/teal.BestMatch-24 381 2910482 ns/op 200 B/op 5 allocs/op
+BenchmarkUnrealFiles/teal.BestMatch-24 382 2844940 ns/op 200 B/op 5 allocs/op
+BenchmarkUnrealFiles/teal.BestMatch-24 390 2819916 ns/op 200 B/op 5 allocs/op
+
+BenchmarkLinuxFiles/isacikgoz.Find-24 36 29633575 ns/op 73632 B/op 368 allocs/op
+BenchmarkLinuxFiles/isacikgoz.Find-24 39 29405028 ns/op 73634 B/op 368 allocs/op
+BenchmarkLinuxFiles/isacikgoz.Find-24 39 29617157 ns/op 73632 B/op 368 allocs/op
+BenchmarkLinuxFiles/isacikgoz.Find-24 39 29922710 ns/op 73634 B/op 368 allocs/op
+BenchmarkLinuxFiles/isacikgoz.Find-24 40 29664292 ns/op 73648 B/op 368 allocs/op
+BenchmarkLinuxFiles/isacikgoz.Find-24 51 29403815 ns/op 73637 B/op 368 allocs/op
+
+BenchmarkLinuxFiles/sahilm.Find-24 70 15967443 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/sahilm.Find-24 70 16319718 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/sahilm.Find-24 72 16534890 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/sahilm.Find-24 74 16189509 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/sahilm.Find-24 79 14999524 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/sahilm.Find-24 84 16200886 ns/op 73520 B/op 366 allocs/op
+
+BenchmarkLinuxFiles/teal.Find-24 148 7276280 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/teal.Find-24 148 7726970 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/teal.Find-24 157 7592565 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/teal.Find-24 159 8123690 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/teal.Find-24 164 7334939 ns/op 73520 B/op 366 allocs/op
+BenchmarkLinuxFiles/teal.Find-24 176 7197796 ns/op 73520 B/op 366 allocs/op
+
+BenchmarkLinuxFiles/teal.BestMatch-24 177 6481958 ns/op 200 B/op 5 allocs/op
+BenchmarkLinuxFiles/teal.BestMatch-24 178 6249032 ns/op 200 B/op 5 allocs/op
+BenchmarkLinuxFiles/teal.BestMatch-24 178 6256040 ns/op 200 B/op 5 allocs/op
+BenchmarkLinuxFiles/teal.BestMatch-24 183 6444893 ns/op 200 B/op 5 allocs/op
+BenchmarkLinuxFiles/teal.BestMatch-24 190 6282288 ns/op 200 B/op 5 allocs/op
+BenchmarkLinuxFiles/teal.BestMatch-24 198 6010804 ns/op 200 B/op 5 allocs/op
```
Matching a pattern against ~60K files from the Linux kernel takes about 30ms.
+The function `BestMatch()` is an memory-optimized version of `Find()` returning only the best match.
+
## Contributing
-Everyone is welcome to contribute. Please send me a pull request or file an issue. I promise
-to respond promptly.
+Everyone is welcome to contribute. Please send pull request or open an issue.
## Credits
-* [@ericpauley](https://github.com/ericpauley) & [@lunixbochs](https://github.com/lunixbochs) contributed Unicode awareness and various performance optimisations.
+- [@ericpauley](https://github.com/ericpauley) & [@lunixbochs](https://github.com/lunixbochs) contributed Unicode awareness and various performance optimisations.
-* The algorithm is based of the awesome work of [forrestthewoods](https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js).
+- The algorithm is based of the awesome work of [forrestthewoods](https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js).
See [this](https://blog.forrestthewoods.com/reverse-engineering-sublime-text-s-fuzzy-match-4cffeed33fdb#.d05n81yjy)
blog post for details of the algorithm.
-* The artwork is by my lovely wife Sanah. It's based on the Go Gopher.
+- The artwork is by my lovely wife Sanah. It's based on the Go Gopher.
-* The Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
+- The Go gopher was designed by Renee French ().
The design is licensed under the Creative Commons 3.0 Attributions license.
## License
The MIT License (MIT)
-Copyright (c) 2017 Sahil Muthoo
+Copyright (c) 2017-2021 Sahil Muthoo and some other contributors
+Copyright (c) 2021 Teal.Finance contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -183,4 +257,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-
diff --git a/_example/main.go b/example/main.go
similarity index 85%
rename from _example/main.go
rename to example/main.go
index 5765522..9024073 100644
--- a/_example/main.go
+++ b/example/main.go
@@ -1,27 +1,29 @@
package main
import (
+ "errors"
"fmt"
- "log"
-
"io/ioutil"
+ "log"
"strings"
-
"time"
"github.com/jroimartin/gocui"
- "github.com/sahilm/fuzzy"
+ "github.com/teal-finance/fuzzy"
)
-var filenamesBytes []byte
-var err error
-
-var filenames []string
-
-var g *gocui.Gui
+var (
+ filenamesBytes []byte
+ filenames []string
+ g *gocui.Gui
+)
func main() {
+ var err error
filenamesBytes, err = ioutil.ReadFile("../testdata/ue4_filenames.txt")
+ if err != nil {
+ filenamesBytes, err = ioutil.ReadFile("testdata/ue4_filenames.txt")
+ }
if err != nil {
panic(err)
}
@@ -54,11 +56,12 @@ func main() {
if err := g.SetKeybinding("", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
log.Panicln(err)
}
+
if err := g.SetKeybinding("", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
log.Panicln(err)
}
- if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
+ if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) {
log.Panicln(err)
}
}
@@ -73,6 +76,7 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error {
}
}
}
+
return nil
}
@@ -86,6 +90,7 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error {
}
}
}
+
return nil
}
@@ -93,6 +98,7 @@ func switchToSideView(g *gocui.Gui, view *gocui.View) error {
if _, err := g.SetCurrentView("finder"); err != nil {
return err
}
+
return nil
}
@@ -100,26 +106,30 @@ func switchToMainView(g *gocui.Gui, view *gocui.View) error {
if _, err := g.SetCurrentView("main"); err != nil {
return err
}
+
return nil
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if v, err := g.SetView("finder", -1, 0, 80, 10); err != nil {
- if err != gocui.ErrUnknownView {
+ if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
+
v.Wrap = true
v.Editable = true
v.Frame = true
v.Title = "Type pattern here. Press -> or <- to switch between panes"
+
if _, err := g.SetCurrentView("finder"); err != nil {
return err
}
+
v.Editor = gocui.EditorFunc(finder)
}
if v, err := g.SetView("main", 79, 0, maxX, maxY-1); err != nil {
- if err != gocui.ErrUnknownView {
+ if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
@@ -131,9 +141,10 @@ func layout(g *gocui.Gui) error {
}
if v, err := g.SetView("results", -1, 3, 79, maxY-1); err != nil {
- if err != gocui.ErrUnknownView {
+ if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
+
v.Editable = false
v.Wrap = true
v.Frame = true
@@ -164,18 +175,20 @@ func finder(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
for _, match := range matches {
for i := 0; i < len(match.Str); i++ {
if contains(i, match.MatchedIndexes) {
- fmt.Fprintf(results, fmt.Sprintf("\033[1m%s\033[0m", string(match.Str[i])))
+ fmt.Fprintf(results, "\033[1m%s\033[0m", string(match.Str[i]))
} else {
- fmt.Fprintf(results, string(match.Str[i]))
+ fmt.Fprint(results, string(match.Str[i]))
}
-
}
fmt.Fprintln(results, "")
}
+
return nil
})
+
case key == gocui.KeySpace:
v.EditWrite(' ')
+
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
g.Update(func(gui *gocui.Gui) error {
@@ -191,15 +204,17 @@ func finder(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
for _, match := range matches {
for i := 0; i < len(match.Str); i++ {
if contains(i, match.MatchedIndexes) {
- fmt.Fprintf(results, fmt.Sprintf("\033[1m%s\033[0m", string(match.Str[i])))
+ fmt.Fprintf(results, "\033[1m%s\033[0m", string(match.Str[i]))
} else {
- fmt.Fprintf(results, string(match.Str[i]))
+ fmt.Fprint(results, string(match.Str[i]))
}
}
fmt.Fprintln(results, "")
}
+
return nil
})
+
case key == gocui.KeyDelete:
v.EditDelete(false)
g.Update(func(gui *gocui.Gui) error {
@@ -215,15 +230,17 @@ func finder(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
for _, match := range matches {
for i := 0; i < len(match.Str); i++ {
if contains(i, match.MatchedIndexes) {
- fmt.Fprintf(results, fmt.Sprintf("\033[1m%s\033[0m", string(match.Str[i])))
+ fmt.Fprintf(results, "\033[1m%s\033[0m", string(match.Str[i]))
} else {
- fmt.Fprintf(results, string(match.Str[i]))
+ fmt.Fprint(results, string(match.Str[i]))
}
}
fmt.Fprintln(results, "")
}
+
return nil
})
+
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
}
@@ -235,5 +252,6 @@ func contains(needle int, haystack []int) bool {
return true
}
}
+
return false
}
diff --git a/fuzzy.go b/fuzzy.go
index bd66ee6..f123da8 100644
--- a/fuzzy.go
+++ b/fuzzy.go
@@ -7,10 +7,15 @@ package fuzzy
import (
"sort"
+ "strings"
"unicode"
"unicode/utf8"
)
+//go:generate go install google.golang.org/protobuf/proto
+//go:generate go install google.golang.org/protobuf/cmd/protoc-gen-go
+//go:generate protoc --go_out=. ./fuzzy.proto
+
// Match represents a matched string.
type Match struct {
// The matched string.
@@ -24,17 +29,19 @@ type Match struct {
}
const (
- firstCharMatchBonus = 10
+ enableFasterCode = true
+ firstCharMatchBonus = 10 // 16
+ caseSensitiveBonus = 1 // 3
+ penaltyUnmatched = 1 // 2
matchFollowingSeparatorBonus = 20
camelCaseMatchBonus = 20
adjacentMatchBonus = 5
unmatchedLeadingCharPenalty = -5
maxUnmatchedLeadingCharPenalty = -15
+ separators = `/-_ .\`
)
-var separators = []rune("/-_ .\\")
-
-// Matches is a slice of Match structs
+// Matches is a slice of Match structures.
type Matches []Match
func (a Matches) Len() int { return len(a) }
@@ -51,13 +58,11 @@ type Source interface {
Len() int
}
+// stringSource is a simple implementation of the Source interface.
type stringSource []string
-func (ss stringSource) String(i int) string {
- return ss[i]
-}
-
-func (ss stringSource) Len() int { return len(ss) }
+func (ss stringSource) String(i int) string { return ss[i] }
+func (ss stringSource) Len() int { return len(ss) }
/*
Find looks up pattern in data and returns matches
@@ -75,131 +80,242 @@ The following types of matches apply a bonus:
* The matched character is adjacent to a previous match.
Penalties are applied for every character in the search string that wasn't matched and all leading
-characters upto the first match.
+characters up to the first match.
*/
-func Find(pattern string, data []string) Matches {
- return FindFrom(pattern, stringSource(data))
+func Find(source string, dictionary []string) Matches {
+ return FindFrom(source, stringSource(dictionary))
}
-/*
-FindFrom is an alternative implementation of Find using a Source
-instead of a list of strings.
-*/
-func FindFrom(pattern string, data Source) Matches {
- if len(pattern) == 0 {
+// BestMatch is an optimized version of Find()
+// assuming input is not empty and returning the best match.
+func BestMatch(source string, dictionary []string) *Match {
+ return BestMatchFrom(source, stringSource(dictionary))
+}
+
+// FindFrom is an alternative implementation of Find
+// using a Source instead of a slice of strings.
+func FindFrom(source string, dictionary Source) (matches Matches) {
+ if source == "" {
return nil
}
- runes := []rune(pattern)
- var matches Matches
- var matchedIndexes []int
- for i := 0; i < data.Len(); i++ {
- var match Match
- match.Str = data.String(i)
- match.Index = i
- if matchedIndexes != nil {
- match.MatchedIndexes = matchedIndexes
+
+ matchedIndexes := make([]int, 0, len(source))
+
+ dicLen := dictionary.Len()
+ for i := 0; i < dicLen; i++ {
+ match := Match{
+ Str: dictionary.String(i),
+ Index: i,
+ MatchedIndexes: matchedIndexes,
+ Score: 0,
+ }
+
+ if match.Compare([]rune(source)) {
+ matches = append(matches, match)
+ matchedIndexes = make([]int, 0, len(source))
} else {
- match.MatchedIndexes = make([]int, 0, len(runes))
+ matchedIndexes = match.MatchedIndexes[:0] // Recycle match index slice
}
- var score int
- patternIndex := 0
- bestScore := -1
- matchedIndex := -1
- currAdjacentMatchBonus := 0
- var last rune
- var lastIndex int
- nextc, nextSize := utf8.DecodeRuneInString(data.String(i))
- var candidate rune
- var candidateSize int
- for j := 0; j < len(data.String(i)); j += candidateSize {
- candidate, candidateSize = nextc, nextSize
- if equalFold(candidate, runes[patternIndex]) {
- score = 0
- if j == 0 {
- score += firstCharMatchBonus
- }
- if unicode.IsLower(last) && unicode.IsUpper(candidate) {
- score += camelCaseMatchBonus
- }
- if j != 0 && isSeparator(last) {
- score += matchFollowingSeparatorBonus
- }
- if len(match.MatchedIndexes) > 0 {
- lastMatch := match.MatchedIndexes[len(match.MatchedIndexes)-1]
- bonus := adjacentCharBonus(lastIndex, lastMatch, currAdjacentMatchBonus)
- score += bonus
- // adjacent matches are incremental and keep increasing based on previous adjacent matches
- // thus we need to maintain the current match bonus
- currAdjacentMatchBonus += bonus
- }
- if score > bestScore {
- bestScore = score
- matchedIndex = j
- }
+ }
+
+ sort.Stable(matches)
+
+ return matches
+}
+
+// BestMatchFrom is an optimized version of FindFrom()
+// assuming input is not empty and returning the best match.
+func BestMatchFrom(source string, dictionary Source) *Match {
+ best := &Match{
+ Str: "",
+ Index: 0,
+ MatchedIndexes: make([]int, 0, len(source)),
+ Score: -1,
+ }
+
+ match := &Match{
+ Str: "",
+ Index: 0,
+ MatchedIndexes: make([]int, 0, len(source)),
+ Score: 0,
+ }
+
+ dicLen := dictionary.Len()
+ for i := 0; i < dicLen; i++ {
+ match.Str = dictionary.String(i)
+ match.MatchedIndexes = match.MatchedIndexes[:0] // Recycle match index slice
+ match.Score = 0
+
+ if match.Compare([]rune(source)) && match.Score > best.Score {
+ best, match = match, best
+ best.Index = i
+ }
+ }
+
+ if best.Score < 0 {
+ return nil
+ }
+
+ return best
+}
+
+// Compare computes the matching between two strings: source and target.
+func Compare(source, target string) *Match {
+ match := Match{
+ Str: target,
+ Index: 0,
+ MatchedIndexes: nil,
+ Score: 0,
+ }
+
+ if match.Compare([]rune(source)) {
+ return &match
+ }
+
+ return nil
+}
+
+// Compare computes the matching between input and target.
+func (match *Match) Compare(sourceRunes []rune) bool {
+ sourceIndex := 0
+ bestScore := -1
+ matchedIndex := -1
+ currAdjacentMatchBonus := 0
+ var last rune
+ var lastIndex int
+ nextTargetRune, nextSize := utf8.DecodeRuneInString(match.Str)
+ var candidate rune
+ var candidateSize int
+
+ for i := 0; i < len(match.Str); i += candidateSize {
+ candidate, candidateSize = nextTargetRune, nextSize
+ if score := equalRuneFold(sourceRunes, sourceIndex, candidate); score > 0 {
+ score = 0
+ if i == 0 {
+ score = firstCharMatchBonus
+ }
+
+ if unicode.IsLower(last) && unicode.IsUpper(candidate) {
+ score += camelCaseMatchBonus
}
- var nextp rune
- if patternIndex < len(runes)-1 {
- nextp = runes[patternIndex+1]
+
+ if i != 0 && isSeparator(last) {
+ score += matchFollowingSeparatorBonus
}
- if j+candidateSize < len(data.String(i)) {
- if data.String(i)[j+candidateSize] < utf8.RuneSelf { // Fast path for ASCII
- nextc, nextSize = rune(data.String(i)[j+candidateSize]), 1
- } else {
- nextc, nextSize = utf8.DecodeRuneInString(data.String(i)[j+candidateSize:])
- }
+
+ if len(match.MatchedIndexes) > 0 {
+ lastMatch := match.MatchedIndexes[len(match.MatchedIndexes)-1]
+ bonus := adjacentCharBonus(lastIndex, lastMatch, currAdjacentMatchBonus)
+ score += bonus
+ // adjacent matches are incremental and keep increasing based on previous adjacent matches
+ // thus we need to maintain the current match bonus
+ currAdjacentMatchBonus += bonus
+ }
+
+ if score > bestScore {
+ bestScore = score
+ matchedIndex = i
+ }
+ }
+
+ var nextSourceRune rune
+ if sourceIndex < len(sourceRunes)-1 {
+ nextSourceRune = sourceRunes[sourceIndex+1]
+ }
+
+ if i+candidateSize < len(match.Str) {
+ if match.Str[i+candidateSize] < utf8.RuneSelf { // Fast path for ASCII
+ nextTargetRune, nextSize = rune(match.Str[i+candidateSize]), 1
} else {
- nextc, nextSize = 0, 0
+ nextTargetRune, nextSize = utf8.DecodeRuneInString(match.Str[i+candidateSize:])
}
- // We apply the best score when we have the next match coming up or when the search string has ended.
- // Tracking when the next match is coming up allows us to exhaustively find the best match and not necessarily
- // the first match.
- // For example given the pattern "tk" and search string "The Black Knight", exhaustively matching allows us
- // to match the second k thus giving this string a higher score.
- if equalFold(nextp, nextc) || nextc == 0 {
- if matchedIndex > -1 {
- if len(match.MatchedIndexes) == 0 {
- penalty := matchedIndex * unmatchedLeadingCharPenalty
- bestScore += max(penalty, maxUnmatchedLeadingCharPenalty)
- }
- match.Score += bestScore
- match.MatchedIndexes = append(match.MatchedIndexes, matchedIndex)
- score = 0
- bestScore = -1
- patternIndex++
+ } else {
+ nextTargetRune, nextSize = 0, 0
+ }
+
+ // We apply the best score when we have the next match coming up or when the search string has ended.
+ // Tracking when the next match is coming up allows us to exhaustively find the best match and not necessarily
+ // the first match.
+ // For example given the pattern "tk" and search string "The Black Knight", exhaustively matching allows us
+ // to match the second k thus giving this string a higher score.
+ if matchedIndex > -1 {
+ if extra := zeroOrFold(nextSourceRune, nextTargetRune); extra > 0 {
+ if len(match.MatchedIndexes) == 0 {
+ penalty := matchedIndex * unmatchedLeadingCharPenalty
+ bestScore += max(penalty, maxUnmatchedLeadingCharPenalty)
}
+ match.Score += bestScore // + extra
+ match.MatchedIndexes = append(match.MatchedIndexes, matchedIndex)
+ bestScore = -1
+ sourceIndex++
}
- lastIndex = j
- last = candidate
}
- // apply penalty for each unmatched character
- penalty := len(match.MatchedIndexes) - len(data.String(i))
- match.Score += penalty
- if len(match.MatchedIndexes) == len(runes) {
- matches = append(matches, match)
- matchedIndexes = nil
- } else {
- matchedIndexes = match.MatchedIndexes[:0] // Recycle match index slice
+
+ lastIndex = i
+ last = candidate
+ }
+
+ // apply penalty for each unmatched character
+ penalty := (len(match.MatchedIndexes) - len(match.Str)) * penaltyUnmatched
+ match.Score += penalty
+
+ return len(match.MatchedIndexes) == len(sourceRunes)
+}
+
+func equalRuneFold(runes []rune, index int, targetRune rune) (score int) {
+ if index >= len(runes) {
+ return 0
+ }
+
+ return equalFold(runes[index], targetRune)
+}
+
+func zeroOrFold(sr, tr rune) (score int) {
+ if tr == 0 {
+ if sr == 0 {
+ return 1
}
+
+ return 1
}
- sort.Stable(matches)
- return matches
+
+ if sr == 0 {
+ return 0
+ }
+
+ return equalFold(sr, tr)
+}
+
+func equalFold(tr, sr rune) (score int) {
+ if enableFasterCode {
+ return equalFoldNew(tr, sr)
+ }
+
+ return equalFoldOld(tr, sr)
}
-// Taken from strings.EqualFold
-func equalFold(tr, sr rune) bool {
+// Taken from strings.EqualFold.
+func equalFoldOld(tr, sr rune) (score int) {
if tr == sr {
- return true
+ return caseSensitiveBonus
}
+
if tr < sr {
tr, sr = sr, tr
}
+
// Fast check for ASCII.
if tr < utf8.RuneSelf {
- // ASCII, and sr is upper case. tr must be lower case.
- if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' {
- return true
+ if isSeparator(tr) && isSeparator(sr) {
+ return 1
}
- return false
+
+ // if targetRune is upper case. sourceRune must be lower case.
+ if sr <= 'Z' && 'A' <= sr && tr == sr+'a'-'A' {
+ return 1
+ }
+
+ return 0
}
// General case. SimpleFold(x) returns the next equivalent rune > x
@@ -208,28 +324,97 @@ func equalFold(tr, sr rune) bool {
for r != sr && r < tr {
r = unicode.SimpleFold(r)
}
- return r == tr
+
+ if r == tr {
+ return 1
+ }
+
+ return 0
}
-func adjacentCharBonus(i int, lastMatch int, currentBonus int) int {
+func equalFoldNew(tr, sr rune) (score int) {
+ if tr == sr {
+ return caseSensitiveBonus
+ }
+
+ if tr < sr {
+ tr, sr = sr, tr
+ }
+
+ // Fast check for ASCII.
+ if tr < utf8.RuneSelf {
+ if tr >= 'a' {
+ if tr <= 'z' {
+ return equalLowerUpperCase(tr, sr)
+ }
+ } else if '0' <= tr && tr <= 'Z' {
+ return 0
+ }
+
+ return fastPunctuationCheck(sr)
+ }
+
+ // General case. SimpleFold(x) returns the next equivalent rune > x
+ // or wraps around to smaller values.
+ r := unicode.SimpleFold(sr)
+ for r != sr && r < tr {
+ r = unicode.SimpleFold(r)
+ }
+
+ if r == tr {
+ return 1
+ }
+
+ return 0
+}
+
+// if tr is lower case. sr must be upper case.
+func equalLowerUpperCase(tr, sr rune) (score int) {
+ if tr == sr+'a'-'A' {
+ return 1
+ }
+
+ return 0
+}
+
+// assumption: r is already in the lower part of the ASCII table.
+func fastPunctuationCheck(r rune) (score int) {
+ if r > 'Z' {
+ if r < 'a' {
+ return 1
+ }
+ } else if r < '0' {
+ return 1
+ }
+
+ return 0
+}
+
+func adjacentCharBonus(i, lastMatch, currentBonus int) int {
if lastMatch == i {
return currentBonus*2 + adjacentMatchBonus
}
+
return 0
}
-func isSeparator(s rune) bool {
+func isSeparator(r rune) bool {
+ if enableFasterCode {
+ return strings.IndexByte(separators, byte(r)) >= 0
+ }
+
for _, sep := range separators {
- if s == sep {
+ if r == sep {
return true
}
}
return false
}
-func max(x int, y int) int {
+func max(x, y int) int {
if x > y {
return x
}
+
return y
}
diff --git a/fuzzy.pb.go b/fuzzy.pb.go
new file mode 100644
index 0000000..7e5560c
--- /dev/null
+++ b/fuzzy.pb.go
@@ -0,0 +1,150 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.27.1
+// protoc v3.12.4
+// source: fuzzy.proto
+
+package fuzzy
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type FindArgs struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Pattern *string `protobuf:"bytes,1,req,name=pattern" json:"pattern,omitempty"`
+ Datas []string `protobuf:"bytes,2,rep,name=datas" json:"datas,omitempty"`
+}
+
+func (x *FindArgs) Reset() {
+ *x = FindArgs{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_fuzzy_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FindArgs) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FindArgs) ProtoMessage() {}
+
+func (x *FindArgs) ProtoReflect() protoreflect.Message {
+ mi := &file_fuzzy_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FindArgs.ProtoReflect.Descriptor instead.
+func (*FindArgs) Descriptor() ([]byte, []int) {
+ return file_fuzzy_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *FindArgs) GetPattern() string {
+ if x != nil && x.Pattern != nil {
+ return *x.Pattern
+ }
+ return ""
+}
+
+func (x *FindArgs) GetDatas() []string {
+ if x != nil {
+ return x.Datas
+ }
+ return nil
+}
+
+var File_fuzzy_proto protoreflect.FileDescriptor
+
+var file_fuzzy_proto_rawDesc = []byte{
+ 0x0a, 0x0b, 0x66, 0x75, 0x7a, 0x7a, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x66,
+ 0x75, 0x7a, 0x7a, 0x79, 0x22, 0x3a, 0x0a, 0x08, 0x46, 0x69, 0x6e, 0x64, 0x41, 0x72, 0x67, 0x73,
+ 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x02, 0x28,
+ 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x61,
+ 0x74, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x64, 0x61, 0x74, 0x61, 0x73,
+ 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2e, 0x2f, 0x66, 0x75, 0x7a, 0x7a, 0x79,
+}
+
+var (
+ file_fuzzy_proto_rawDescOnce sync.Once
+ file_fuzzy_proto_rawDescData = file_fuzzy_proto_rawDesc
+)
+
+func file_fuzzy_proto_rawDescGZIP() []byte {
+ file_fuzzy_proto_rawDescOnce.Do(func() {
+ file_fuzzy_proto_rawDescData = protoimpl.X.CompressGZIP(file_fuzzy_proto_rawDescData)
+ })
+ return file_fuzzy_proto_rawDescData
+}
+
+var file_fuzzy_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_fuzzy_proto_goTypes = []interface{}{
+ (*FindArgs)(nil), // 0: fuzzy.FindArgs
+}
+var file_fuzzy_proto_depIdxs = []int32{
+ 0, // [0:0] is the sub-list for method output_type
+ 0, // [0:0] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_fuzzy_proto_init() }
+func file_fuzzy_proto_init() {
+ if File_fuzzy_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_fuzzy_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FindArgs); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_fuzzy_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 1,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_fuzzy_proto_goTypes,
+ DependencyIndexes: file_fuzzy_proto_depIdxs,
+ MessageInfos: file_fuzzy_proto_msgTypes,
+ }.Build()
+ File_fuzzy_proto = out.File
+ file_fuzzy_proto_rawDesc = nil
+ file_fuzzy_proto_goTypes = nil
+ file_fuzzy_proto_depIdxs = nil
+}
diff --git a/fuzzy.proto b/fuzzy.proto
new file mode 100644
index 0000000..29e9848
--- /dev/null
+++ b/fuzzy.proto
@@ -0,0 +1,8 @@
+ syntax = "proto2";
+ package fuzzy;
+ option go_package = "../fuzzy";
+
+ message FindArgs {
+ required string pattern = 1;
+ repeated string datas = 2;
+ }
diff --git a/fuzzy_test.go b/fuzzy_test.go
index 2585c21..0025d8d 100644
--- a/fuzzy_test.go
+++ b/fuzzy_test.go
@@ -1,36 +1,70 @@
-package fuzzy_test
+package fuzzy
import (
- "testing"
-
- "github.com/sahilm/fuzzy"
-
+ "context"
+ "fmt"
"io/ioutil"
+ "sort"
"strings"
-
- "fmt"
+ "testing"
"time"
+ "unicode"
+ "unicode/utf8"
+ isacikgoz "github.com/isacikgoz/fuzzy"
"github.com/kylelemons/godebug/pretty"
+ sahilm "github.com/sahilm/fuzzy"
+ "google.golang.org/protobuf/proto"
)
+func FuzzFuzzyFind(data []byte) int {
+ args := &FindArgs{
+ Pattern: new(string),
+ Datas: []string{},
+ }
+
+ err := proto.Unmarshal(data, args)
+ if err != nil {
+ return 0
+ }
+
+ matches := Find(*args.Pattern, args.Datas)
+ for _, match := range matches {
+ for i := 0; i < len(match.Str); i++ {
+ for _, j := range match.MatchedIndexes {
+ if j == i {
+ // fmt.Printf("found %#+v\n", match)
+ break
+ }
+ }
+ }
+ }
+
+ return 1
+}
+
func TestFindWithUnicode(t *testing.T) {
- matches := fuzzy.Find("\U0001F41D", []string{"\U0001F41D"})
+ matches := Find("\U0001F41D", []string{"\U0001F41D"})
if len(matches) != 1 {
t.Errorf("got %v Matches; expected 1 match", len(matches))
}
+
+ best := BestMatch("\U0001F41D", []string{"\U0001F41D"})
+ if best == nil {
+ t.Error("got best=nil; expected 1 match")
+ }
}
func TestFindWithCannedData(t *testing.T) {
cases := []struct {
pattern string
data []string
- matches []fuzzy.Match
+ matches []Match
}{
// first char bonus, camel case bonuses and unmatched chars penalty
// (m = 10, n = 20, r = 20) - 18 unmatched chars = 32
{
- "mnr", []string{"moduleNameResolver.ts"}, []fuzzy.Match{
+ "mnr", []string{"moduleNameResolver.ts"}, []Match{
{
Str: "moduleNameResolver.ts",
Index: 0,
@@ -40,7 +74,7 @@ func TestFindWithCannedData(t *testing.T) {
},
},
{
- "mmt", []string{"mémeTemps"}, []fuzzy.Match{
+ "mmt", []string{"mémeTemps"}, []Match{
{
Str: "mémeTemps",
Index: 0,
@@ -51,7 +85,7 @@ func TestFindWithCannedData(t *testing.T) {
},
// ranking
{
- "mnr", []string{"moduleNameResolver.ts", "my name is_Ramsey"}, []fuzzy.Match{
+ "mnr", []string{"moduleNameResolver.ts", "my name is_Ramsey"}, []Match{
{
Str: "my name is_Ramsey",
Index: 1,
@@ -68,7 +102,7 @@ func TestFindWithCannedData(t *testing.T) {
},
// simple repeated pattern and adjacent match bonus
{
- "aaa", []string{"aaa", "bbb"}, []fuzzy.Match{
+ "aaa", []string{"aaa", "bbb"}, []Match{
{
Str: "aaa",
Index: 0,
@@ -79,7 +113,7 @@ func TestFindWithCannedData(t *testing.T) {
},
// exhaustive matching
{
- "tk", []string{"The Black Knight"}, []fuzzy.Match{
+ "tk", []string{"The Black Knight"}, []Match{
{
Str: "The Black Knight",
Index: 0,
@@ -90,15 +124,15 @@ func TestFindWithCannedData(t *testing.T) {
},
// any unmatched char in the pattern removes the whole match
{
- "cats", []string{"cat"}, []fuzzy.Match{},
+ "cats", []string{"cat"}, []Match{},
},
// empty patterns return no Matches
{
- "", []string{"cat"}, []fuzzy.Match{},
+ "", []string{"cat"}, []Match{},
},
// separator bonus
{
- "abcx", []string{"abc\\x"}, []fuzzy.Match{
+ "abcx", []string{"abc\\x"}, []Match{
{
Str: "abc\\x",
Index: 0,
@@ -108,14 +142,61 @@ func TestFindWithCannedData(t *testing.T) {
},
},
}
+
for _, c := range cases {
- matches := fuzzy.Find(c.pattern, c.data)
- if len(matches) != len(c.matches) {
- t.Errorf("got %v Matches; expected %v match", len(matches), len(c.matches))
- }
- if diff := pretty.Compare(c.matches, matches); diff != "" {
- t.Errorf("%v", diff)
- }
+ t.Run("sahilm.Find("+c.pattern+")", func(t *testing.T) {
+ matches := sahilm.Find(c.pattern, c.data)
+
+ if len(matches) != len(c.matches) {
+ t.Errorf("got %v Matches; expected %v match", len(matches), len(c.matches))
+ }
+ if diff := pretty.Compare(c.matches, matches); diff != "" {
+ t.Errorf("%v", diff)
+ }
+ })
+
+ t.Run("isacikgoz.Find("+c.pattern+")", func(t *testing.T) {
+ channel := isacikgoz.Find(context.Background(), c.pattern, c.data)
+ matches := make([]isacikgoz.Match, 0)
+ for match := range channel {
+ matches = append(matches, match)
+ }
+ sort.Stable(isacikgoz.Sortable(matches))
+
+ if len(matches) != len(c.matches) {
+ t.Errorf("got %v Matches; expected %v match", len(matches), len(c.matches))
+ }
+ if diff := pretty.Compare(c.matches, matches); diff != "" {
+ t.Errorf("%v", diff)
+ }
+ })
+
+ t.Run("teal.Find("+c.pattern+")", func(t *testing.T) {
+ matches := Find(c.pattern, c.data)
+
+ if len(matches) != len(c.matches) {
+ t.Errorf("got %v Matches; expected %v match", len(matches), len(c.matches))
+ }
+ if diff := pretty.Compare(c.matches, matches); diff != "" {
+ t.Errorf("%v", diff)
+ }
+ })
+
+ t.Run("teal.Best("+c.pattern+")", func(t *testing.T) {
+ best := BestMatch(c.pattern, c.data)
+
+ if best == nil && len(c.matches) > 0 {
+ t.Errorf("got best=%v ; expected %v match", best, len(c.matches))
+ }
+ if best != nil && len(c.matches) == 0 {
+ t.Errorf("got best=%v ; expected %v match", best, len(c.matches))
+ }
+ if best != nil && len(c.matches) > 0 {
+ if diff := pretty.Compare(c.matches[0], best); diff != "" {
+ t.Errorf("%v", diff)
+ }
+ }
+ })
}
}
@@ -145,23 +226,46 @@ func TestFindFromSource(t *testing.T) {
name: "Allie",
},
}
- want := fuzzy.Matches{
- {
+
+ want := Matches{
+ Match{
Str: "Allie",
Index: 2,
MatchedIndexes: []int{0, 1},
Score: 12,
- }, {
+ }, Match{
Str: "Alice",
Index: 0,
MatchedIndexes: []int{0, 1},
Score: 12,
},
}
- got := fuzzy.FindFrom("al", emps)
- if diff := pretty.Compare(want, got); diff != "" {
- t.Errorf("%v", diff)
- }
+
+ t.Run("sahilm.FindFrom", func(t *testing.T) {
+ got := sahilm.FindFrom("al", emps)
+ if diff := pretty.Compare(want, got); diff != "" {
+ t.Errorf("%v", diff)
+ }
+ })
+
+ t.Run("isacikgoz.FindFrom", func(t *testing.T) {
+ channel := isacikgoz.FindFrom(context.Background(), "al", emps)
+ got := make([]isacikgoz.Match, 0)
+ for match := range channel {
+ got = append(got, match)
+ }
+ sort.Stable(isacikgoz.Sortable(got))
+ if diff := pretty.Compare(want, got); diff != "" {
+ t.Errorf("%v", diff)
+ }
+ })
+
+ t.Run("teal.FindFrom", func(t *testing.T) {
+ got := FindFrom("al", emps)
+ if diff := pretty.Compare(want, got); diff != "" {
+ t.Errorf("%v", diff)
+ }
+ })
}
func TestFindWithRealworldData(t *testing.T) {
@@ -171,7 +275,6 @@ func TestFindWithRealworldData(t *testing.T) {
numMatches int
filenames []string
}{
-
{
"ue4", 4, []string{
"UE4Game.cpp",
@@ -205,9 +308,16 @@ func TestFindWithRealworldData(t *testing.T) {
for _, c := range cases {
now := time.Now()
- matches := fuzzy.Find(c.pattern, filenames)
+ matches := Find(c.pattern, filenames)
elapsed := time.Since(now)
- fmt.Printf("Matching '%v' in Unreal 4... found %v Matches in %v\n", c.pattern, len(matches), elapsed)
+
+ if matches == nil || len(matches) < c.numMatches {
+ t.Errorf("Got matches=%v ; want at least %v", len(matches), c.numMatches)
+ continue
+ }
+
+ t.Logf("Matching '%v' in Unreal 4 found %v Matches in %v\n", c.pattern, len(matches), elapsed)
+
foundfilenames := make([]string, 0)
for i := 0; i < c.numMatches; i++ {
foundfilenames = append(foundfilenames, matches[i].Str)
@@ -215,6 +325,14 @@ func TestFindWithRealworldData(t *testing.T) {
if diff := pretty.Compare(c.filenames, foundfilenames); diff != "" {
t.Errorf("%v", diff)
}
+
+ now = time.Now()
+ best := BestMatch(c.pattern, filenames)
+ elapsed = time.Since(now)
+ t.Logf("Best '%v' in Unreal 4 in %v\n", c.pattern, elapsed)
+ if best == nil {
+ t.Error("Got best=nil ; expected a match")
+ }
}
})
@@ -252,9 +370,10 @@ func TestFindWithRealworldData(t *testing.T) {
for _, c := range cases {
now := time.Now()
- matches := fuzzy.Find(c.pattern, filenames)
+ matches := Find(c.pattern, filenames)
elapsed := time.Since(now)
- fmt.Printf("Matching '%v' in linux kernel... found %v Matches in %v\n", c.pattern, len(matches), elapsed)
+ t.Logf("Matching '%v' in linux kernel found %v Matches in %v\n", c.pattern, len(matches), elapsed)
+
foundfilenames := make([]string, 0)
if len(matches) < c.numMatches {
t.Fatal("Too few Matches")
@@ -265,33 +384,405 @@ func TestFindWithRealworldData(t *testing.T) {
if diff := pretty.Compare(c.filenames, foundfilenames); diff != "" {
t.Errorf("%v", diff)
}
+
+ now = time.Now()
+ best := BestMatch(c.pattern, filenames)
+ elapsed = time.Since(now)
+ t.Logf("Best '%v' in Unreal 4 in %v\n", c.pattern, elapsed)
+ if best == nil {
+ t.Error("Got best=nil ; expected a match")
+ }
+ }
+ })
+}
+
+func BenchmarkUnrealFiles(b *testing.B) {
+ b.Log("~16K files from unreal 4")
+
+ bytes, err := ioutil.ReadFile("testdata/ue4_filenames.txt")
+ if err != nil {
+ b.Fatal(err)
+ }
+ filenames := strings.Split(string(bytes), "\n")
+
+ b.Run("isacikgoz.Find", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ channel := isacikgoz.Find(context.Background(), "lll", filenames)
+ matches := make([]isacikgoz.Match, 0)
+ for match := range channel {
+ matches = append(matches, match)
+ }
+ sort.Stable(isacikgoz.Sortable(matches))
+ }
+ })
+
+ b.Run("sahilm.Find", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ sahilm.Find("lll", filenames)
}
})
+ b.Run("teal.Find", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Find("lll", filenames)
+ }
+ })
+
+ b.Run("teal.BestMatch", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ BestMatch("lll", filenames)
+ }
+ })
}
-func BenchmarkFind(b *testing.B) {
- b.Run("with unreal 4 (~16K files)", func(b *testing.B) {
- bytes, err := ioutil.ReadFile("testdata/ue4_filenames.txt")
- if err != nil {
- b.Fatal(err)
+func BenchmarkLinuxFiles(b *testing.B) {
+ b.Log("~60K files from Linux kernel")
+
+ bytes, err := ioutil.ReadFile("testdata/linux_filenames.txt")
+ if err != nil {
+ b.Fatal(err)
+ }
+ filenames := strings.Split(string(bytes), "\n")
+
+ b.Run("isacikgoz.Find", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ channel := isacikgoz.Find(context.Background(), "lll", filenames)
+ matches := make([]isacikgoz.Match, 0)
+ for match := range channel {
+ matches = append(matches, match)
+ }
+ sort.Stable(isacikgoz.Sortable(matches))
}
- filenames := strings.Split(string(bytes), "\n")
- b.ResetTimer()
+ })
+
+ b.Run("sahilm.Find", func(b *testing.B) {
for i := 0; i < b.N; i++ {
- fuzzy.Find("lll", filenames)
+ sahilm.Find("lll", filenames)
}
})
- b.Run("with linux kernel (~60K files)", func(b *testing.B) {
- bytes, err := ioutil.ReadFile("testdata/linux_filenames.txt")
- if err != nil {
- b.Fatal(err)
+ b.Run("teal.Find", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Find("lll", filenames)
}
- filenames := strings.Split(string(bytes), "\n")
- b.ResetTimer()
+ })
+
+ b.Run("teal.BestMatch", func(b *testing.B) {
for i := 0; i < b.N; i++ {
- fuzzy.Find("alsa", filenames)
+ BestMatch("lll", filenames)
}
})
}
+
+type testCase struct{ source, want string }
+
+func initDictionary(kind string) []string {
+ switch kind {
+ default:
+ return []string{
+ /*0*/ "Limit Book",
+ /*1*/ "Order Book by Limit",
+ /*2*/ "Full Book",
+ /*3*/ "Full Order Book",
+ /*4*/ "BinanceJersey",
+ /*5*/ "Binance Jersey",
+ "LILI_BOBO", "limik-tobo", "LimikTobo", "LIMIK-BOTO", "TILIM KOOB", "tilim-koob", "tilimkoob",
+ "LUFL KOBO", "LUFLBOKO", "lufl.kobo", "lufl boko", "LuflKobo", "Lufl Kobo", "LufL-KoBo", "LufL KooB",
+ "King Gizzard", "The Lizard Wizard", "Lizzard Wizzard",
+ }
+ case "lower":
+ return []string{
+ /*0*/ "limit book",
+ /*1*/ "order book by limit",
+ /*2*/ "full book",
+ /*3*/ "full order book",
+ /*4*/ "binancejersey",
+ /*5*/ "binance jersey",
+ "lili_bobo", "limik-tobo", "limiktobo", "limik-boto", "tilim koob", "tilim-koob", "tilimkoob",
+ "lufl kobo", "luflboko", "lufl.kobo", "lufl boko", "luflkobo", "lufl kobo", "lufl-kobo", "lufl koob",
+ "king gizzard", "the lizard wizard", "lizzard wizzard",
+ }
+ case "upper":
+ return []string{
+ /*0*/ "LIMIT BOOK",
+ /*1*/ "ORDER BOOK BY LIMIT",
+ /*2*/ "FULL BOOK",
+ /*3*/ "FULL ORDER BOOK",
+ /*4*/ "BINANCEJERSEY",
+ /*5*/ "BINANCE JERSEY",
+ "LILI_BOBO", "LIMIK-TOBO", "LIMIKTOBO", "LIMIK-BOTO", "TILIM KOOB", "TILIM-KOOB", "TILIMKOOB",
+ "LUFL KOBO", "LUFLBOKO", "LUFL.KOBO", "LUFL BOKO", "LUFLKOBO", "LUFL KOBO", "LUFL-KOBO", "LUFL KOOB",
+ "KING GIZZARD", "THE LIZARD WIZARD", "LIZZARD WIZZARD",
+ }
+ }
+}
+
+func initTestCases(dictionary []string) []testCase {
+ return []testCase{
+ {source: "limit", want: dictionary[0]},
+ {source: "LImit", want: dictionary[0]},
+ {source: "Limit", want: dictionary[0]},
+ {source: "Book by", want: dictionary[1]},
+ {source: "Full", want: dictionary[2]},
+ {source: "ul Boo", want: dictionary[2]},
+ {source: "ul ord", want: dictionary[3]},
+ {source: "FullBook", want: dictionary[2]},
+ {source: "fullbook", want: dictionary[2]},
+ {source: "full-book", want: dictionary[2]},
+ {source: "full.book", want: dictionary[2]},
+ {source: "full/book", want: dictionary[2]},
+ {source: "FULL_BOOK", want: dictionary[2]},
+ {source: "LimitBook", want: dictionary[0]},
+ {source: "limit-book", want: dictionary[0]},
+ {source: "LIMIT_BOOK", want: dictionary[0]},
+ {source: "LIMIT.BOOK", want: dictionary[0]},
+ {source: "LIMIT/BOOK", want: dictionary[0]},
+ {source: "BINANCE_JERSEY", want: dictionary[5]},
+ }
+}
+
+func testFind(t *testing.T, source, want string, dico []string) {
+ t.Helper()
+
+ matches := Find(source, dico)
+
+ if matches == nil {
+ t.Errorf("source=%q got=nil want=%q", source, want)
+
+ return
+ }
+
+ if len(matches) == 0 {
+ t.Errorf("source=%q got=empty want=%q", source, want)
+
+ return
+ }
+
+ if got := dico[matches[0].Index]; got != want {
+ t.Errorf("source=%q got=%q want=%q", source, matches[0].Index, want)
+ t.Logf("matches=%+v", matches)
+ }
+}
+
+func testBest(t *testing.T, source, want string, dico []string) {
+ t.Helper()
+
+ best := BestMatch(source, dico)
+
+ if best == nil {
+ t.Errorf("source=%q got=nil want=%q", source, want)
+
+ return
+ }
+
+ if got := dico[best.Index]; got != want {
+ t.Errorf("source=%q got={index:%v str:%q} want=%q", source, best.Index, dico[best.Index], want)
+ t.Logf("best=%+v", best)
+ }
+}
+
+func TestUpperLowerCases(t *testing.T) {
+ dictionaries := map[string][]string{
+ "vanilla": initDictionary("vanilla"),
+ "lower": initDictionary("lower"),
+ "upper": initDictionary("upper"),
+ }
+
+ for kind, dictionary := range dictionaries {
+ cases := initTestCases(dictionary)
+
+ for _, c := range cases {
+ t.Run(kind+"/Find="+c.source, func(t *testing.T) {
+ testFind(t, c.source, c.want, dictionary)
+ })
+ t.Run(kind+"/find="+c.source, func(t *testing.T) {
+ testFind(t, strings.ToLower(c.source), c.want, dictionary)
+ })
+ t.Run(kind+"/FIND="+c.source, func(t *testing.T) {
+ testFind(t, strings.ToUpper(c.source), c.want, dictionary)
+ })
+
+ t.Run(kind+"/Best="+c.source, func(t *testing.T) {
+ testBest(t, c.source, c.want, dictionary)
+ })
+ t.Run(kind+"/best="+c.source, func(t *testing.T) {
+ testBest(t, strings.ToLower(c.source), c.want, dictionary)
+ })
+ t.Run(kind+"/BEST="+c.source, func(t *testing.T) {
+ testBest(t, strings.ToUpper(c.source), c.want, dictionary)
+ })
+ }
+ }
+}
+
+func TestMatch_Compare(t *testing.T) {
+ cases := []struct {
+ source string
+ target string
+ want bool
+ }{
+ {source: "Full Book", target: "FULL_BOOK", want: true},
+ {source: "Full Book", target: "full-book", want: true},
+ {source: "Full Book", target: "full.book", want: true},
+ {source: "Full Book", target: "full/book", want: true},
+ /* TODO fail
+ {source: "full book", target: "FullBook", want: true},
+ {source: "Full Book", target: "fullbook", want: true},
+ {source: "Full Book", target: "FullBook", want: true},
+ {source: "FULL BOOK", target: "FullBook", want: true},
+ */
+ {source: "FULL_BOOK", target: "Full Book", want: true},
+ {source: "FULL_BOOK", target: "full.book", want: true},
+ {source: "full-book", target: "Full Book", want: true},
+ /* TODO fail
+ {source: "full-book", target: "FullBook", want: true},
+ */
+ {source: "full.book", target: "Full Book", want: true},
+ {source: "full.book", target: "FULL_BOOK", want: true},
+ {source: "full.book", target: "full/book", want: true},
+ {source: "full/book", target: "Full Book", want: true},
+ {source: "full/book", target: "full.book", want: true},
+ {source: "fullbook", target: "Full Book", want: true},
+ {source: "FullBook", target: "full book", want: true},
+ {source: "FullBook", target: "Full Book", want: true},
+ {source: "FullBook", target: "FULL BOOK", want: true},
+ {source: "FullBook", target: "full-book", want: true},
+ {source: "Limit Book", target: "LIMIT_BOOK", want: true},
+ {source: "Limit Book", target: "limit-book", want: true},
+ {source: "Limit Book", target: "LIMIT.BOOK", want: true},
+ {source: "Limit Book", target: "LIMIT/BOOK", want: true},
+ /* TODO fail
+ {source: "Limit Book", target: "LimitBook", want: true},
+ */
+ {source: "LIMIT_BOOK", target: "Limit Book", want: true},
+ {source: "limit-book", target: "Limit Book", want: true},
+ {source: "LIMIT.BOOK", target: "Limit Book", want: true},
+ {source: "LIMIT/BOOK", target: "Limit Book", want: true},
+ {source: "LimitBook", target: "Limit Book", want: true},
+ }
+
+ for _, c := range cases {
+ t.Run(c.source+"=="+c.target, func(t *testing.T) {
+ match := Match{
+ Str: c.target,
+ Index: 0,
+ MatchedIndexes: nil,
+ Score: 0,
+ }
+
+ got := match.Compare([]rune(c.source))
+
+ if got != c.want {
+ t.Errorf("source=%q target=%q got=%v want=%v", c.source, c.target, got, c.want)
+ t.Logf("match=%+v", match)
+ }
+ })
+ }
+}
+
+func rStr(r rune) string {
+ s := string(r)
+
+ if r < ' ' || !utf8.ValidRune(r) {
+ s = fmt.Sprintf("%#x", r)
+ }
+
+ return s
+}
+
+func Test_equalFold_range(t *testing.T) {
+ const punctuation = "[]{}|^~_" + "\\" + "\u007f"
+
+ for r1 := rune('0'); r1 < rune(9999); r1++ {
+ r2 := r1 + 'a' - 'A'
+
+ name := string(r1) + "==" + string(r2)
+ t.Run(name, func(t *testing.T) {
+ r3 := unicode.SimpleFold(r1)
+ for r3 != r1 && r3 < r2 {
+ r3 = unicode.SimpleFold(r3)
+ }
+
+ want := 0
+ if r3 == r2 {
+ want = 1
+ } else if strings.ContainsRune(punctuation, r1) && strings.ContainsRune(punctuation, r2) {
+ return // want = 1
+ }
+
+ if testEqualFold(t, r1, r2, want) {
+ testEqualFold(t, r2, r1, want)
+ }
+ })
+ }
+}
+
+func Test_equalFold(t *testing.T) {
+ cases := []struct {
+ sr rune
+ tr rune
+ want int
+ }{
+ {'a', 'a', caseSensitiveBonus},
+ {'-', '-', caseSensitiveBonus},
+ {'3', '3', caseSensitiveBonus},
+ {'*', '*', caseSensitiveBonus},
+ {'R', 'R', caseSensitiveBonus},
+ {' ', 'a', 0},
+ {'a', 'A', 1},
+ {'Z', 'z', 1},
+ {'a', 'z', 0},
+ {'A', 'z', 0},
+ {'"', 'z', 0},
+ {'#', 'A', 0},
+ {'$', '9', 0},
+ {'(', '@', 0},
+ {'*', '1', 0},
+ {'-', '←', 0},
+ {'-', '_', 1},
+ {'.', '↑', 0},
+ {'/', 'a', 0},
+ {'1', '_', 0},
+ {'E', '.', 0},
+ {'û', 'x', 0},
+ {'û', '*', 0},
+ {'û', ' ', 0},
+ {'à', 'a', 0},
+ {'à', 'À', 1},
+ {'ç', 'c', 0},
+ {'ç', 'Ç', 1},
+ // {')', '\\', 1},
+ // {'+', '`', 1},
+ // {'&', '_', 1},
+ // {'%', ' ', 1},
+ // {'!', ' ', 1},
+ // {',', '^', 1},
+ // {'/', '~', 1},
+ // {'\'', ']', 1},
+ // {'', '/', 1},
+ }
+
+ for _, c := range cases {
+ name := string(c.sr) + "==" + string(c.tr)
+ t.Run(name, func(t *testing.T) {
+ if testEqualFold(t, c.sr, c.tr, c.want) {
+ testEqualFold(t, c.tr, c.sr, c.want)
+ }
+ })
+ }
+}
+
+func testEqualFold(t *testing.T, sr, tr rune, want int) bool {
+ t.Helper()
+
+ if got := equalFoldOld(sr, tr); got != want {
+ t.Errorf("equalFoldOld(%v %v) = %v, want %v", rStr(sr), rStr(tr), got, want)
+ }
+
+ if got := equalFoldNew(sr, tr); got != want {
+ t.Errorf("equalFoldNew(%v %v) = %v, want %v", rStr(sr), rStr(tr), got, want)
+ return false
+ }
+
+ return true
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b4e7e98
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,12 @@
+module github.com/teal-finance/fuzzy
+
+go 1.16
+
+require (
+ github.com/isacikgoz/fuzzy v0.2.0 // indirect
+ github.com/jroimartin/gocui v0.5.0
+ github.com/kylelemons/godebug v1.1.0
+ github.com/mattn/go-runewidth v0.0.13 // indirect
+ github.com/sahilm/fuzzy v0.1.0 // indirect
+ google.golang.org/protobuf v1.27.1
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..a41e88b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,24 @@
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/isacikgoz/fuzzy v0.2.0 h1:b2AUOLrmR36em9UhkWMkIrEJZFeoPgl9kZzBiktpntU=
+github.com/isacikgoz/fuzzy v0.2.0/go.mod h1:VEYn1Gfwj4lMg+FTH603LmQni/zTrhxKv7nTFG+RO8U=
+github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
+github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
+github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
+github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=