├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bytes.go ├── bytes_test.go ├── go.mod ├── go.sum ├── lib.go ├── lib_test.go ├── string.go └── string_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - 1.12 6 | - 1.13 7 | - tip 8 | 9 | script: 10 | - go test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-20 Carlos Cobo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # substring [![Build Status](https://travis-ci.org/toqueteos/substring.png?branch=master)](https://travis-ci.org/toqueteos/substring) [![GoDoc](http://godoc.org/github.com/toqueteos/substring?status.png)](http://godoc.org/github.com/toqueteos/substring) 2 | 3 | Very fast **one-time string searches** in Go. Simple and composable. 4 | 5 | Interop with [regexp](http://golang.org/pkg/regexp/) for backwards compatibility (easy migration from your current system to `substring`). 6 | 7 | ## Installation 8 | 9 | The recommended way to install substring is by using `go get`: 10 | 11 | ``` 12 | go get github.com/toqueteos/substring 13 | ``` 14 | 15 | Go Modules are supported! 16 | 17 | ## Examples 18 | 19 | A basic example with two matchers: 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "regexp" 27 | 28 | "github.com/toqueteos/substring/v2" 29 | ) 30 | 31 | func main() { 32 | m1 := substring.After("assets/", substring.Or( 33 | substring.Has("jquery"), 34 | substring.Has("angular"), 35 | substring.Suffixes(".js", ".css", ".html"), 36 | )) 37 | fmt.Println(m1.Match("assets/angular/foo/bar")) // Prints: true 38 | fmt.Println(m1.Match("assets/js/file.js")) // Prints: true 39 | fmt.Println(m1.Match("assets/style/bar.css")) // Prints: true 40 | fmt.Println(m1.Match("assets/foo/bar.html")) // Prints: true 41 | fmt.Println(m1.Match("assets/js/qux.json")) // Prints: false 42 | fmt.Println(m1.Match("core/file.html")) // Prints: false 43 | fmt.Println(m1.Match("foobar/that.jsx")) // Prints: false 44 | fmt.Println() 45 | 46 | m2 := substring.After("vendor/", substring.Suffixes(".css", ".js", ".less")) 47 | fmt.Println(m2.Match("foo/vendor/bar/qux.css")) // Prints: true 48 | fmt.Println(m2.Match("foo/var/qux.less")) // Prints: false 49 | fmt.Println() 50 | 51 | re := regexp.MustCompile(`vendor\/.*\.(css|js|less)$`) 52 | fmt.Println(re.MatchString("foo/vendor/bar/qux.css")) // Prints: true 53 | fmt.Println(re.MatchString("foo/var/qux.less")) // Prints: false 54 | } 55 | ``` 56 | 57 | ## How fast? 58 | 59 | It may vary depending on your use case but 1~2 orders of magnitude faster than `regexp` is pretty common. 60 | 61 | Test it out for yourself by running `go test -bench .`! 62 | 63 | ``` 64 | $ go test -bench . 65 | pkg: github.com/toqueteos/substring 66 | BenchmarkExample1-16 30759529 38.4 ns/op 67 | BenchmarkExample2-16 26659675 40.0 ns/op 68 | BenchmarkExample3-16 30760317 37.7 ns/op 69 | BenchmarkExample4-16 31566652 36.8 ns/op 70 | BenchmarkExample5-16 123704845 9.70 ns/op 71 | BenchmarkExampleRe1-16 2739574 436 ns/op 72 | BenchmarkExampleRe2-16 2494791 480 ns/op 73 | BenchmarkExampleRe3-16 1681654 713 ns/op 74 | BenchmarkExampleRe4-16 2205490 540 ns/op 75 | BenchmarkExampleRe5-16 19673001 55.0 ns/op 76 | PASS 77 | ok github.com/toqueteos/substring 15.016s 78 | ``` 79 | 80 | ## License 81 | 82 | MIT, see [LICENSE](LICENSE) 83 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package substring 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | 7 | "github.com/toqueteos/trie" 8 | ) 9 | 10 | type BytesMatcher interface { 11 | Match(b []byte) bool 12 | MatchIndex(b []byte) int 13 | } 14 | 15 | // regexp 16 | type regexpBytes struct{ re *regexp.Regexp } 17 | 18 | func BytesRegexp(pat string) *regexpBytes { return ®expBytes{regexp.MustCompile(pat)} } 19 | func (m *regexpBytes) Match(b []byte) bool { return m.re.Match(b) } 20 | func (m *regexpBytes) MatchIndex(b []byte) int { 21 | found := m.re.FindIndex(b) 22 | if found != nil { 23 | return found[1] 24 | } 25 | return -1 26 | } 27 | 28 | // exact 29 | type exactBytes struct{ pat []byte } 30 | 31 | func BytesExact(pat string) *exactBytes { return &exactBytes{[]byte(pat)} } 32 | func (m *exactBytes) Match(b []byte) bool { 33 | l, r := len(m.pat), len(b) 34 | if l != r { 35 | return false 36 | } 37 | for i := 0; i < l; i++ { 38 | if b[i] != m.pat[i] { 39 | return false 40 | } 41 | } 42 | return true 43 | } 44 | func (m *exactBytes) MatchIndex(b []byte) int { 45 | if m.Match(b) { 46 | return len(b) 47 | } 48 | return -1 49 | } 50 | 51 | // any, search `s` in `.Match(pat)` 52 | type anyBytes struct { 53 | pat []byte 54 | } 55 | 56 | func BytesAny(pat string) *anyBytes { return &anyBytes{[]byte(pat)} } 57 | func (m *anyBytes) Match(b []byte) bool { return bytes.Index(m.pat, b) >= 0 } 58 | func (m *anyBytes) MatchIndex(b []byte) int { 59 | if idx := bytes.Index(m.pat, b); idx >= 0 { 60 | return idx + len(b) 61 | } 62 | return -1 63 | } 64 | 65 | // has, search `pat` in `.Match(s)` 66 | type hasBytes struct { 67 | pat []byte 68 | } 69 | 70 | func BytesHas(pat string) *hasBytes { return &hasBytes{[]byte(pat)} } 71 | func (m *hasBytes) Match(b []byte) bool { return bytes.Index(b, m.pat) >= 0 } 72 | func (m *hasBytes) MatchIndex(b []byte) int { 73 | if idx := bytes.Index(b, m.pat); idx >= 0 { 74 | return idx + len(m.pat) 75 | } 76 | return -1 77 | } 78 | 79 | // prefix 80 | type prefixBytes struct{ pat []byte } 81 | 82 | func BytesPrefix(pat string) *prefixBytes { return &prefixBytes{[]byte(pat)} } 83 | func (m *prefixBytes) Match(b []byte) bool { return bytes.HasPrefix(b, m.pat) } 84 | func (m *prefixBytes) MatchIndex(b []byte) int { 85 | if bytes.HasPrefix(b, m.pat) { 86 | return len(m.pat) 87 | } 88 | return -1 89 | } 90 | 91 | // prefixes 92 | type prefixesBytes struct { 93 | t *trie.Trie 94 | } 95 | 96 | func BytesPrefixes(pats ...string) *prefixesBytes { 97 | t := trie.New() 98 | for _, pat := range pats { 99 | t.Insert([]byte(pat)) 100 | } 101 | return &prefixesBytes{t} 102 | } 103 | func (m *prefixesBytes) Match(b []byte) bool { return m.t.PrefixIndex(b) >= 0 } 104 | func (m *prefixesBytes) MatchIndex(b []byte) int { 105 | if idx := m.t.PrefixIndex(b); idx >= 0 { 106 | return idx 107 | } 108 | return -1 109 | } 110 | 111 | // suffix 112 | type suffixBytes struct{ pat []byte } 113 | 114 | func BytesSuffix(pat string) *suffixBytes { return &suffixBytes{[]byte(pat)} } 115 | func (m *suffixBytes) Match(b []byte) bool { return bytes.HasSuffix(b, m.pat) } 116 | func (m *suffixBytes) MatchIndex(b []byte) int { 117 | if bytes.HasSuffix(b, m.pat) { 118 | return len(m.pat) 119 | } 120 | return -1 121 | } 122 | 123 | // suffixes 124 | type suffixesBytes struct { 125 | t *trie.Trie 126 | } 127 | 128 | func BytesSuffixes(pats ...string) *suffixesBytes { 129 | t := trie.New() 130 | for _, pat := range pats { 131 | t.Insert(reverse([]byte(pat))) 132 | } 133 | return &suffixesBytes{t} 134 | } 135 | func (m *suffixesBytes) Match(b []byte) bool { 136 | return m.t.PrefixIndex(reverse(b)) >= 0 137 | } 138 | func (m *suffixesBytes) MatchIndex(b []byte) int { 139 | if idx := m.t.PrefixIndex(reverse(b)); idx >= 0 { 140 | return idx 141 | } 142 | return -1 143 | } 144 | 145 | // after 146 | type afterBytes struct { 147 | first []byte 148 | matcher BytesMatcher 149 | } 150 | 151 | func BytesAfter(first string, m BytesMatcher) *afterBytes { return &afterBytes{[]byte(first), m} } 152 | func (a *afterBytes) Match(b []byte) bool { 153 | if idx := bytes.Index(b, a.first); idx >= 0 { 154 | return a.matcher.Match(b[idx+len(a.first):]) 155 | } 156 | return false 157 | } 158 | func (a *afterBytes) MatchIndex(b []byte) int { 159 | if idx := bytes.Index(b, a.first); idx >= 0 { 160 | return idx + a.matcher.MatchIndex(b[idx:]) 161 | } 162 | return -1 163 | } 164 | 165 | // and, returns true iff all matchers return true 166 | type andBytes struct{ matchers []BytesMatcher } 167 | 168 | func BytesAnd(m ...BytesMatcher) *andBytes { return &andBytes{m} } 169 | func (a *andBytes) Match(b []byte) bool { 170 | for _, m := range a.matchers { 171 | if !m.Match(b) { 172 | return false 173 | } 174 | } 175 | return true 176 | } 177 | func (a *andBytes) MatchIndex(b []byte) int { 178 | longest := 0 179 | for _, m := range a.matchers { 180 | if idx := m.MatchIndex(b); idx < 0 { 181 | return -1 182 | } else if idx > longest { 183 | longest = idx 184 | } 185 | } 186 | return longest 187 | } 188 | 189 | // or, returns true iff any matcher returns true 190 | type orBytes struct{ matchers []BytesMatcher } 191 | 192 | func BytesOr(m ...BytesMatcher) *orBytes { return &orBytes{m} } 193 | func (o *orBytes) Match(b []byte) bool { 194 | for _, m := range o.matchers { 195 | if m.Match(b) { 196 | return true 197 | } 198 | } 199 | return false 200 | } 201 | func (o *orBytes) MatchIndex(b []byte) int { 202 | for _, m := range o.matchers { 203 | if idx := m.MatchIndex(b); idx >= 0 { 204 | return idx 205 | } 206 | } 207 | return -1 208 | } 209 | 210 | type suffixGroupBytes struct { 211 | suffix BytesMatcher 212 | matchers []BytesMatcher 213 | } 214 | 215 | func BytesSuffixGroup(s string, m ...BytesMatcher) *suffixGroupBytes { 216 | return &suffixGroupBytes{BytesSuffix(s), m} 217 | } 218 | func (sg *suffixGroupBytes) Match(b []byte) bool { 219 | if sg.suffix.Match(b) { 220 | return BytesOr(sg.matchers...).Match(b) 221 | } 222 | return false 223 | } 224 | func (sg *suffixGroupBytes) MatchIndex(b []byte) int { 225 | if sg.suffix.MatchIndex(b) >= 0 { 226 | return BytesOr(sg.matchers...).MatchIndex(b) 227 | } 228 | return -1 229 | } 230 | -------------------------------------------------------------------------------- /bytes_test.go: -------------------------------------------------------------------------------- 1 | package substring 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/matryer/is" 7 | ) 8 | 9 | func TestBytesAny(t *testing.T) { 10 | is := is.New(t) 11 | 12 | a := BytesAny("foo") // search s in foo 13 | is.Equal(a.MatchIndex([]byte("f")), 1) 14 | is.Equal(a.MatchIndex([]byte("foo")), 3) 15 | is.Equal(a.MatchIndex([]byte("foobar")), -1) 16 | is.Equal(a.MatchIndex([]byte("p")), -1) 17 | } 18 | 19 | func TestBytesHas(t *testing.T) { 20 | is := is.New(t) 21 | 22 | h := BytesHas("foo") // search foo in s 23 | is.Equal(h.MatchIndex([]byte("foo")), 3) 24 | is.Equal(h.MatchIndex([]byte("foobar")), 3) 25 | is.Equal(h.MatchIndex([]byte("f")), -1) 26 | } 27 | 28 | func TestBytesPrefix(t *testing.T) { 29 | is := is.New(t) 30 | 31 | p := BytesPrefix("foo") 32 | is.True(p.Match([]byte("foo"))) 33 | is.True(p.Match([]byte("foobar"))) 34 | is.Equal(p.Match([]byte("barfoo")), false) 35 | is.Equal(p.Match([]byte(" foo")), false) 36 | is.Equal(p.Match([]byte("bar")), false) 37 | is.Equal(p.MatchIndex([]byte("foo")), 3) 38 | is.Equal(p.MatchIndex([]byte("foobar")), 3) 39 | is.Equal(p.MatchIndex([]byte("barfoo")), -1) 40 | is.Equal(p.MatchIndex([]byte(" foo")), -1) 41 | is.Equal(p.MatchIndex([]byte("bar")), -1) 42 | ps := BytesPrefixes("foo", "barfoo") 43 | is.True(ps.Match([]byte("foo"))) 44 | is.True(ps.Match([]byte("barfoo"))) 45 | is.Equal(ps.Match([]byte("qux")), false) 46 | is.Equal(ps.MatchIndex([]byte("foo")), 2) 47 | is.Equal(ps.MatchIndex([]byte("barfoo")), 5) 48 | is.Equal(ps.MatchIndex([]byte("qux")), -1) 49 | } 50 | 51 | func TestBytesSuffix(t *testing.T) { 52 | is := is.New(t) 53 | 54 | p := BytesSuffix("foo") 55 | is.True(p.Match([]byte("foo"))) 56 | is.True(p.Match([]byte("barfoo"))) 57 | is.Equal(p.Match([]byte("foobar")), false) 58 | is.Equal(p.Match([]byte("foo ")), false) 59 | is.Equal(p.Match([]byte("bar")), false) 60 | is.Equal(p.MatchIndex([]byte("foo")), 3) 61 | is.Equal(p.MatchIndex([]byte("barfoo")), 3) 62 | is.Equal(p.MatchIndex([]byte("foobar")), -1) 63 | is.Equal(p.MatchIndex([]byte("foo ")), -1) 64 | is.Equal(p.MatchIndex([]byte("bar")), -1) 65 | ps := BytesSuffixes("foo", "foobar") 66 | is.True(ps.Match([]byte("foo"))) 67 | is.True(ps.Match([]byte("foobar"))) 68 | is.Equal(ps.Match([]byte("qux")), false) 69 | is.Equal(ps.MatchIndex([]byte("foo")), 2) 70 | is.Equal(ps.MatchIndex([]byte("foobar")), 5) 71 | is.Equal(ps.MatchIndex([]byte("qux")), -1) 72 | ps2 := BytesSuffixes(".foo", ".bar", ".qux") 73 | is.True(ps2.Match([]byte("bar.foo"))) 74 | is.Equal(ps2.Match([]byte("bar.js")), false) 75 | is.True(ps2.Match([]byte("foo/foo.bar"))) 76 | is.Equal(ps2.Match([]byte("foo/foo.js")), false) 77 | is.True(ps2.Match([]byte("foo/foo/bar.qux"))) 78 | is.Equal(ps2.Match([]byte("foo/foo/bar.css")), false) 79 | } 80 | 81 | func TestBytesExact(t *testing.T) { 82 | is := is.New(t) 83 | 84 | a := BytesExact("foo") 85 | is.True(a.Match([]byte("foo"))) 86 | is.Equal(a.Match([]byte("bar")), false) 87 | is.Equal(a.Match([]byte("qux")), false) 88 | } 89 | 90 | func TestBytesAfter(t *testing.T) { 91 | is := is.New(t) 92 | 93 | a1 := BytesAfter("foo", BytesExact("bar")) 94 | is.True(a1.Match([]byte("foobar"))) 95 | is.Equal(a1.Match([]byte("foo_bar")), false) 96 | a2 := BytesAfter("foo", BytesHas("bar")) 97 | is.True(a2.Match([]byte("foobar"))) 98 | is.True(a2.Match([]byte("foo_bar"))) 99 | is.True(a2.Match([]byte("_foo_bar"))) 100 | is.Equal(a2.Match([]byte("foo_nope")), false) 101 | is.Equal(a2.Match([]byte("qux")), false) 102 | a3 := BytesAfter("foo", BytesPrefixes("bar", "qux")) 103 | is.True(a3.Match([]byte("foobar"))) 104 | is.True(a3.Match([]byte("fooqux"))) 105 | is.Equal(a3.Match([]byte("foo bar")), false) 106 | is.Equal(a3.Match([]byte("foo_qux")), false) 107 | } 108 | 109 | func TestBytesSuffixGroup(t *testing.T) { 110 | is := is.New(t) 111 | 112 | sg1 := BytesSuffixGroup(".foo", BytesHas("bar")) 113 | is.True(sg1.Match([]byte("bar.foo"))) 114 | is.True(sg1.Match([]byte("barqux.foo"))) 115 | is.Equal(sg1.Match([]byte(".foo.bar")), false) 116 | sg2 := BytesSuffixGroup(`.foo`, 117 | BytesAfter(`bar`, BytesHas("qux")), 118 | ) 119 | is.True(sg2.Match([]byte("barqux.foo"))) 120 | is.True(sg2.Match([]byte("barbarqux.foo"))) 121 | is.Equal(sg2.Match([]byte("bar.foo")), false) 122 | is.Equal(sg2.Match([]byte("foo.foo")), false) 123 | sg3 := BytesSuffixGroup(`.foo`, 124 | BytesAfter(`bar`, BytesRegexp(`\d+`)), 125 | ) 126 | is.True(sg3.Match([]byte("bar0.foo"))) 127 | is.Equal(sg3.Match([]byte("bar.foo")), false) 128 | is.Equal(sg3.Match([]byte("bar0.qux")), false) 129 | } 130 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/toqueteos/substring/v2 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/matryer/is v1.2.0 7 | github.com/toqueteos/trie v1.0.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 2 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 3 | github.com/toqueteos/trie v1.0.0 h1:8i6pXxNUXNRAqP246iibb7w/pSFquNTQ+uNfriG7vlk= 4 | github.com/toqueteos/trie v1.0.0/go.mod h1:Ywk48QhEqhU1+DwhMkJ2x7eeGxDHiGkAdc9+0DYcbsM= 5 | -------------------------------------------------------------------------------- /lib.go: -------------------------------------------------------------------------------- 1 | package substring 2 | 3 | // reverse is a helper fn for Suffixes 4 | func reverse(b []byte) []byte { 5 | n := len(b) 6 | for i := 0; i < n/2; i++ { 7 | b[i], b[n-1-i] = b[n-1-i], b[i] 8 | } 9 | return b 10 | } 11 | -------------------------------------------------------------------------------- /lib_test.go: -------------------------------------------------------------------------------- 1 | package substring 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | var matcher = After("vendor/", Suffixes(".css", ".js", ".less")) 9 | 10 | func BenchmarkExample1(b *testing.B) { 11 | for i := 0; i < b.N; i++ { 12 | matcher.Match("foo/vendor/bar/qux.css") 13 | } 14 | } 15 | func BenchmarkExample2(b *testing.B) { 16 | for i := 0; i < b.N; i++ { 17 | matcher.Match("foo/vendor/bar.foo/qux.css") 18 | } 19 | } 20 | func BenchmarkExample3(b *testing.B) { 21 | for i := 0; i < b.N; i++ { 22 | matcher.Match("foo/vendor/bar.foo/qux.jsx") 23 | } 24 | } 25 | func BenchmarkExample4(b *testing.B) { 26 | for i := 0; i < b.N; i++ { 27 | matcher.Match("foo/vendor/bar/qux.jsx") 28 | } 29 | } 30 | func BenchmarkExample5(b *testing.B) { 31 | for i := 0; i < b.N; i++ { 32 | matcher.Match("foo/var/qux.less") 33 | } 34 | } 35 | 36 | var re = regexp.MustCompile(`vendor\/.*\.(css|js|less)$`) 37 | 38 | func BenchmarkExampleRe1(b *testing.B) { 39 | for i := 0; i < b.N; i++ { 40 | re.MatchString("foo/vendor/bar/qux.css") 41 | } 42 | } 43 | func BenchmarkExampleRe2(b *testing.B) { 44 | for i := 0; i < b.N; i++ { 45 | re.MatchString("foo/vendor/bar.foo/qux.css") 46 | } 47 | } 48 | func BenchmarkExampleRe3(b *testing.B) { 49 | for i := 0; i < b.N; i++ { 50 | re.MatchString("foo/vendor/bar.foo/qux.jsx") 51 | } 52 | } 53 | func BenchmarkExampleRe4(b *testing.B) { 54 | for i := 0; i < b.N; i++ { 55 | re.MatchString("foo/vendor/bar/qux.jsx") 56 | } 57 | } 58 | func BenchmarkExampleRe5(b *testing.B) { 59 | for i := 0; i < b.N; i++ { 60 | re.MatchString("foo/var/qux.less") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package substring 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/toqueteos/trie" 8 | ) 9 | 10 | type StringsMatcher interface { 11 | Match(s string) bool 12 | MatchIndex(s string) int 13 | } 14 | 15 | // regexp 16 | type regexpString struct{ re *regexp.Regexp } 17 | 18 | func Regexp(pat string) *regexpString { return ®expString{regexp.MustCompile(pat)} } 19 | func (m *regexpString) Match(s string) bool { return m.re.MatchString(s) } 20 | func (m *regexpString) MatchIndex(s string) int { 21 | found := m.re.FindStringIndex(s) 22 | if found != nil { 23 | return found[1] 24 | } 25 | return -1 26 | } 27 | 28 | // exact 29 | type exactString struct{ pat string } 30 | 31 | func Exact(pat string) *exactString { return &exactString{pat} } 32 | func (m *exactString) Match(s string) bool { return m.pat == s } 33 | func (m *exactString) MatchIndex(s string) int { 34 | if m.pat == s { 35 | return len(s) 36 | } 37 | return -1 38 | } 39 | 40 | // any, search `s` in `.Match(pat)` 41 | type anyString struct{ pat string } 42 | 43 | func Any(pat string) *anyString { return &anyString{pat} } 44 | func (m *anyString) Match(s string) bool { 45 | return strings.Index(m.pat, s) >= 0 46 | } 47 | func (m *anyString) MatchIndex(s string) int { 48 | if idx := strings.Index(m.pat, s); idx >= 0 { 49 | return idx + len(s) 50 | } 51 | return -1 52 | } 53 | 54 | // has, search `pat` in `.Match(s)` 55 | type hasString struct{ pat string } 56 | 57 | func Has(pat string) *hasString { return &hasString{pat} } 58 | func (m *hasString) Match(s string) bool { 59 | return strings.Index(s, m.pat) >= 0 60 | } 61 | func (m *hasString) MatchIndex(s string) int { 62 | if idx := strings.Index(s, m.pat); idx >= 0 { 63 | return idx + len(m.pat) 64 | } 65 | return -1 66 | } 67 | 68 | // prefix 69 | type prefixString struct{ pat string } 70 | 71 | func Prefix(pat string) *prefixString { return &prefixString{pat} } 72 | func (m *prefixString) Match(s string) bool { return strings.HasPrefix(s, m.pat) } 73 | func (m *prefixString) MatchIndex(s string) int { 74 | if strings.HasPrefix(s, m.pat) { 75 | return len(m.pat) 76 | } 77 | return -1 78 | } 79 | 80 | // prefixes 81 | type prefixesString struct{ t *trie.Trie } 82 | 83 | func Prefixes(pats ...string) *prefixesString { 84 | t := trie.New() 85 | for _, pat := range pats { 86 | t.Insert([]byte(pat)) 87 | } 88 | return &prefixesString{t} 89 | } 90 | func (m *prefixesString) Match(s string) bool { return m.t.PrefixIndex([]byte(s)) >= 0 } 91 | func (m *prefixesString) MatchIndex(s string) int { 92 | if idx := m.t.PrefixIndex([]byte(s)); idx >= 0 { 93 | return idx 94 | } 95 | return -1 96 | } 97 | 98 | // suffix 99 | type suffixString struct{ pat string } 100 | 101 | func Suffix(pat string) *suffixString { return &suffixString{pat} } 102 | func (m *suffixString) Match(s string) bool { return strings.HasSuffix(s, m.pat) } 103 | func (m *suffixString) MatchIndex(s string) int { 104 | if strings.HasSuffix(s, m.pat) { 105 | return len(m.pat) 106 | } 107 | return -1 108 | } 109 | 110 | // suffixes 111 | type suffixesString struct{ t *trie.Trie } 112 | 113 | func Suffixes(pats ...string) *suffixesString { 114 | t := trie.New() 115 | for _, pat := range pats { 116 | t.Insert(reverse([]byte(pat))) 117 | } 118 | return &suffixesString{t} 119 | } 120 | func (m *suffixesString) Match(s string) bool { 121 | return m.t.PrefixIndex(reverse([]byte(s))) >= 0 122 | } 123 | func (m *suffixesString) MatchIndex(s string) int { 124 | if idx := m.t.PrefixIndex(reverse([]byte(s))); idx >= 0 { 125 | return idx 126 | } 127 | return -1 128 | } 129 | 130 | // after 131 | type afterString struct { 132 | first string 133 | matcher StringsMatcher 134 | } 135 | 136 | func After(first string, m StringsMatcher) *afterString { 137 | return &afterString{first, m} 138 | } 139 | func (a *afterString) Match(s string) bool { 140 | if idx := strings.Index(s, a.first); idx >= 0 { 141 | return a.matcher.Match(s[idx+len(a.first):]) 142 | } 143 | return false 144 | } 145 | func (a *afterString) MatchIndex(s string) int { 146 | if idx := strings.Index(s, a.first); idx >= 0 { 147 | return idx + a.matcher.MatchIndex(s[idx+len(a.first):]) 148 | } 149 | return -1 150 | } 151 | 152 | // and, returns true iff all matchers return true 153 | type andString struct{ matchers []StringsMatcher } 154 | 155 | func And(m ...StringsMatcher) *andString { return &andString{m} } 156 | func (a *andString) Match(s string) bool { 157 | for _, m := range a.matchers { 158 | if !m.Match(s) { 159 | return false 160 | } 161 | } 162 | return true 163 | } 164 | func (a *andString) MatchIndex(s string) int { 165 | longest := 0 166 | for _, m := range a.matchers { 167 | if idx := m.MatchIndex(s); idx < 0 { 168 | return -1 169 | } else if idx > longest { 170 | longest = idx 171 | } 172 | } 173 | return longest 174 | } 175 | 176 | // or, returns true iff any matcher returns true 177 | type orString struct{ matchers []StringsMatcher } 178 | 179 | func Or(m ...StringsMatcher) *orString { return &orString{m} } 180 | func (o *orString) Match(s string) bool { 181 | for _, m := range o.matchers { 182 | if m.Match(s) { 183 | return true 184 | } 185 | } 186 | return false 187 | } 188 | func (o *orString) MatchIndex(s string) int { 189 | for _, m := range o.matchers { 190 | if idx := m.MatchIndex(s); idx >= 0 { 191 | return idx 192 | } 193 | } 194 | return -1 195 | } 196 | 197 | type suffixGroupString struct { 198 | suffix StringsMatcher 199 | matchers []StringsMatcher 200 | } 201 | 202 | func SuffixGroup(s string, m ...StringsMatcher) *suffixGroupString { 203 | return &suffixGroupString{Suffix(s), m} 204 | } 205 | func (sg *suffixGroupString) Match(s string) bool { 206 | if sg.suffix.Match(s) { 207 | return Or(sg.matchers...).Match(s) 208 | } 209 | return false 210 | } 211 | func (sg *suffixGroupString) MatchIndex(s string) int { 212 | if sg.suffix.MatchIndex(s) >= 0 { 213 | return Or(sg.matchers...).MatchIndex(s) 214 | } 215 | return -1 216 | } 217 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package substring 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/matryer/is" 7 | ) 8 | 9 | func TestAny(t *testing.T) { 10 | is := is.New(t) 11 | 12 | a := Any("foo") // search s in foo 13 | is.Equal(a.MatchIndex("f"), 1) 14 | is.Equal(a.MatchIndex("foo"), 3) 15 | is.Equal(a.MatchIndex("foobar"), -1) 16 | is.Equal(a.MatchIndex("p"), -1) 17 | } 18 | 19 | func TestHas(t *testing.T) { 20 | is := is.New(t) 21 | 22 | h := Has("foo") // search foo in s 23 | is.Equal(h.MatchIndex("foo"), 3) 24 | is.Equal(h.MatchIndex("foobar"), 3) 25 | is.Equal(h.MatchIndex("f"), -1) 26 | } 27 | 28 | func TestPrefix(t *testing.T) { 29 | is := is.New(t) 30 | 31 | p := Prefix("foo") 32 | is.True(p.Match("foo")) 33 | is.True(p.Match("foobar")) 34 | is.Equal(p.Match("barfoo"), false) 35 | is.Equal(p.Match(" foo"), false) 36 | is.Equal(p.Match("bar"), false) 37 | is.Equal(p.MatchIndex("foo"), 3) 38 | is.Equal(p.MatchIndex("foobar"), 3) 39 | is.Equal(p.MatchIndex("barfoo"), -1) 40 | is.Equal(p.MatchIndex(" foo"), -1) 41 | is.Equal(p.MatchIndex("bar"), -1) 42 | ps := Prefixes("foo", "barfoo") 43 | is.True(ps.Match("foo")) 44 | is.True(ps.Match("barfoo")) 45 | is.Equal(ps.Match("qux"), false) 46 | is.Equal(ps.MatchIndex("foo"), 2) 47 | is.Equal(ps.MatchIndex("barfoo"), 5) 48 | is.Equal(ps.MatchIndex("qux"), -1) 49 | } 50 | 51 | func TestSuffix(t *testing.T) { 52 | is := is.New(t) 53 | 54 | p := Suffix("foo") 55 | is.True(p.Match("foo")) 56 | is.True(p.Match("barfoo")) 57 | is.Equal(p.Match("foobar"), false) 58 | is.Equal(p.Match("foo "), false) 59 | is.Equal(p.Match("bar"), false) 60 | is.Equal(p.MatchIndex("foo"), 3) 61 | is.Equal(p.MatchIndex("barfoo"), 3) 62 | is.Equal(p.MatchIndex("foobar"), -1) 63 | is.Equal(p.MatchIndex("foo "), -1) 64 | is.Equal(p.MatchIndex("bar"), -1) 65 | ps1 := Suffixes("foo", "foobar") 66 | is.True(ps1.Match("foo")) 67 | is.True(ps1.Match("foobar")) 68 | is.Equal(ps1.Match("qux"), false) 69 | is.Equal(ps1.MatchIndex("foo"), 2) 70 | is.Equal(ps1.MatchIndex("foobar"), 5) 71 | is.Equal(ps1.MatchIndex("qux"), -1) 72 | ps2 := Suffixes(".foo", ".bar", ".qux") 73 | is.True(ps2.Match("bar.foo")) 74 | is.Equal(ps2.Match("bar.js"), false) 75 | is.True(ps2.Match("foo/foo.bar")) 76 | is.Equal(ps2.Match("foo/foo.js"), false) 77 | is.True(ps2.Match("foo/foo/bar.qux")) 78 | is.Equal(ps2.Match("foo/foo/bar.css"), false) 79 | } 80 | 81 | func TestExact(t *testing.T) { 82 | is := is.New(t) 83 | 84 | a := Exact("foo") 85 | is.True(a.Match("foo")) 86 | is.Equal(a.Match("bar"), false) 87 | is.Equal(a.Match("qux"), false) 88 | } 89 | 90 | func TestAfter(t *testing.T) { 91 | is := is.New(t) 92 | 93 | a1 := After("foo", Exact("bar")) 94 | is.True(a1.Match("foobar")) 95 | is.Equal(a1.Match("foo_bar"), false) 96 | a2 := After("foo", Has("bar")) 97 | is.True(a2.Match("foobar")) 98 | is.True(a2.Match("foo_bar")) 99 | is.True(a2.Match("_foo_bar")) 100 | is.Equal(a2.Match("foo_nope"), false) 101 | is.Equal(a2.Match("qux"), false) 102 | a3 := After("foo", Prefixes("bar", "qux")) 103 | is.True(a3.Match("foobar")) 104 | is.True(a3.Match("fooqux")) 105 | is.Equal(a3.Match("foo bar"), false) 106 | is.Equal(a3.Match("foo_qux"), false) 107 | } 108 | 109 | func TestSuffixGroup(t *testing.T) { 110 | is := is.New(t) 111 | 112 | sg1 := SuffixGroup(".foo", Has("bar")) 113 | is.True(sg1.Match("bar.foo")) 114 | is.True(sg1.Match("barqux.foo")) 115 | is.Equal(sg1.Match(".foo.bar"), false) 116 | sg2 := SuffixGroup(`.foo`, 117 | After(`bar`, Has("qux")), 118 | ) 119 | is.True(sg2.Match("barqux.foo")) 120 | is.True(sg2.Match("barbarqux.foo")) 121 | is.Equal(sg2.Match("bar.foo"), false) 122 | is.Equal(sg2.Match("foo.foo"), false) 123 | sg3 := SuffixGroup(`.foo`, 124 | After(`bar`, Regexp(`\d+`)), 125 | ) 126 | is.True(sg3.Match("bar0.foo")) 127 | is.Equal(sg3.Match("bar.foo"), false) 128 | is.Equal(sg3.Match("bar0.qux"), false) 129 | } 130 | --------------------------------------------------------------------------------