├── .gitignore ├── go.mod ├── .github └── workflows │ ├── bench.yml │ └── main.yml ├── Makefile ├── go.sum ├── bench ├── kkdai_radix.go ├── dghubble_trie.go ├── armon_go_radix.go ├── targets.go ├── go.mod ├── hashicorp_go_immutable_radix.go ├── acomagu_trie.go ├── tchap_go_patricia.go ├── derekparker_trie.go ├── sauerbraten_radix.go ├── go.sum ├── targets_test.go └── bench_test.go ├── LICENSE ├── example_test.go ├── CREDITS ├── trie.go ├── README.md └── trie_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | bench/enwiki-latest-all-titles-in-ns0.gz 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/acomagu/trie/v2 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/matryer/is v1.2.0 7 | golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 8 | ) 9 | -------------------------------------------------------------------------------- /.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: bench 2 | on: [push] 3 | jobs: 4 | bench: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-go@v3 9 | - run: make bench 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-go@v3 9 | - run: go test -v ./... 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bench 2 | bench: bench/enwiki-latest-all-titles-in-ns0.gz 3 | (cd bench && go test -bench . -benchmem .) 4 | 5 | bench/enwiki-latest-all-titles-in-ns0.gz: 6 | curl https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-all-titles-in-ns0.gz -o bench/enwiki-latest-all-titles-in-ns0.gz 7 | -------------------------------------------------------------------------------- /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 | golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw= 4 | golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 5 | -------------------------------------------------------------------------------- /bench/kkdai_radix.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/kkdai/radix" 5 | ) 6 | 7 | type kkdaiRadix struct { 8 | tree *radix.RadixTree 9 | } 10 | 11 | func (t *kkdaiRadix) Name() string { 12 | return "github.com/kkdai/radix" 13 | } 14 | 15 | func (t *kkdaiRadix) Build(keys [][]byte, values []interface{}) { 16 | t.tree = radix.NewRadixTree() 17 | for i := range keys { 18 | t.tree.Insert(string(keys[i]), values[i]) 19 | } 20 | } 21 | 22 | func (t *kkdaiRadix) Get(s []byte) (interface{}, bool) { 23 | return t.tree.Lookup(string(s)) 24 | } 25 | -------------------------------------------------------------------------------- /bench/dghubble_trie.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import "github.com/dghubble/trie" 4 | 5 | type dghubbleTrie struct { 6 | tree *trie.RuneTrie 7 | } 8 | 9 | func (t *dghubbleTrie) Name() string { 10 | return "github.com/dghubble/trie" 11 | } 12 | 13 | func (t *dghubbleTrie) Build(keys [][]byte, values []interface{}) { 14 | t.tree = trie.NewRuneTrie() 15 | for i := range keys { 16 | t.tree.Put(string(keys[i]), values[i]) 17 | } 18 | } 19 | 20 | func (t *dghubbleTrie) Get(s []byte) (interface{}, bool) { 21 | v := t.tree.Get(string(s)) 22 | return v, v != nil 23 | } 24 | -------------------------------------------------------------------------------- /bench/armon_go_radix.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/armon/go-radix" 5 | ) 6 | 7 | type armonGoRadix struct { 8 | tree *radix.Tree 9 | } 10 | 11 | func (t *armonGoRadix) Name() string { 12 | return "github.com/armon/go-radix" 13 | } 14 | 15 | func (t *armonGoRadix) Build(keys [][]byte, values []interface{}) { 16 | t.tree = radix.New() 17 | for i := range keys { 18 | t.tree.Insert(string(keys[i]), values[i]) 19 | } 20 | } 21 | 22 | func (t *armonGoRadix) Get(s []byte) (interface{}, bool) { 23 | return t.tree.Get(string(s)) 24 | } 25 | 26 | func (t *armonGoRadix) LongestPrefix(s []byte) (interface{}, bool) { 27 | _, v, match := t.tree.LongestPrefix(string(s)) 28 | return v, match 29 | } 30 | -------------------------------------------------------------------------------- /bench/targets.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | type target interface { 4 | Name() string 5 | Build([][]byte, []interface{}) 6 | Get([]byte) (interface{}, bool) 7 | } 8 | 9 | type targetHasLongestPrefix interface { 10 | target 11 | LongestPrefix([]byte) (interface{}, bool) 12 | } 13 | 14 | var targetFactories = []func() target{ 15 | func() target { return new(acomaguTrie) }, 16 | func() target { return new(armonGoRadix) }, 17 | func() target { return new(hashicorpGoImmutableRadix) }, 18 | func() target { return new(tchapGoPatricia) }, 19 | func() target { return new(kkdaiRadix) }, 20 | func() target { return new(sauerbratenRadix) }, 21 | func() target { return new(dghubbleTrie) }, 22 | func() target { return new(derekparkerTrie) }, 23 | } 24 | -------------------------------------------------------------------------------- /bench/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/acomagu/trie/bench 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/acomagu/trie/v2 v2.0.0 7 | github.com/armon/go-radix v1.0.0 8 | github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9 9 | github.com/dghubble/trie v0.0.0-20190512033633-6d8e3fa705df 10 | github.com/hashicorp/go-immutable-radix v1.1.0 11 | github.com/kkdai/radix v0.0.0-20181128172204-f0c88ccaf15e 12 | github.com/matryer/is v1.2.0 13 | github.com/sauerbraten/radix v0.0.0-20150210222551-4445e9cd8982 14 | github.com/tchap/go-patricia v2.3.0+incompatible 15 | ) 16 | 17 | require ( 18 | github.com/hashicorp/golang-lru v0.5.0 // indirect 19 | golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect 20 | ) 21 | 22 | replace github.com/acomagu/trie/v2 v2.0.0 => ../ 23 | -------------------------------------------------------------------------------- /bench/hashicorp_go_immutable_radix.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | iradix "github.com/hashicorp/go-immutable-radix" 5 | ) 6 | 7 | type hashicorpGoImmutableRadix struct { 8 | tree *iradix.Tree 9 | } 10 | 11 | func (t *hashicorpGoImmutableRadix) Name() string { 12 | return "github.com/hashicorp/go-immutable-radix" 13 | } 14 | 15 | func (t *hashicorpGoImmutableRadix) Build(keys [][]byte, values []interface{}) { 16 | t.tree = iradix.New() 17 | for i := range keys { 18 | t.tree, _, _ = t.tree.Insert(keys[i], values[i]) 19 | } 20 | } 21 | 22 | func (t *hashicorpGoImmutableRadix) Get(s []byte) (interface{}, bool) { 23 | return t.tree.Get(s) 24 | } 25 | 26 | func (t *hashicorpGoImmutableRadix) LongestPrefix(s []byte) (interface{}, bool) { 27 | root := t.tree.Root() 28 | _, v, matched := root.LongestPrefix(s) 29 | return v, matched 30 | } 31 | -------------------------------------------------------------------------------- /bench/acomagu_trie.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/acomagu/trie/v2" 5 | ) 6 | 7 | type acomaguTrie struct { 8 | tree trie.Tree[byte, any] 9 | } 10 | 11 | func (t *acomaguTrie) Name() string { 12 | return "github.com/acomagu/trie" 13 | } 14 | 15 | func (t *acomaguTrie) Build(keys [][]byte, values []interface{}) { 16 | t.tree = trie.New(keys, values) 17 | } 18 | 19 | func (t *acomaguTrie) Get(s []byte) (interface{}, bool) { 20 | return t.tree.Trace(s).Terminal() 21 | } 22 | 23 | func (t *acomaguTrie) LongestPrefix(s []byte) (interface{}, bool) { 24 | var v interface{} 25 | var match bool 26 | 27 | tt := t.tree 28 | for _, c := range s { 29 | tt = tt.TraceOne(c) 30 | if tt == nil { 31 | break 32 | } 33 | if vv, ok := tt.Terminal(); ok { 34 | v = vv 35 | match = true 36 | } 37 | } 38 | 39 | return v, match 40 | } 41 | -------------------------------------------------------------------------------- /bench/tchap_go_patricia.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/tchap/go-patricia/patricia" 5 | ) 6 | 7 | type tchapGoPatricia struct { 8 | tree *patricia.Trie 9 | } 10 | 11 | func (t *tchapGoPatricia) Name() string { 12 | return "github.com/tchap/go-patricia/patricia" 13 | } 14 | 15 | func (t *tchapGoPatricia) Build(keys [][]byte, values []interface{}) { 16 | t.tree = patricia.NewTrie() 17 | for i := range keys { 18 | t.tree.Insert(patricia.Prefix(keys[i]), values[i]) 19 | } 20 | } 21 | 22 | func (t *tchapGoPatricia) Get(s []byte) (interface{}, bool) { 23 | itm := t.tree.Get(s) 24 | return itm, itm != nil 25 | } 26 | 27 | func (t *tchapGoPatricia) LongestPrefix(src []byte) (interface{}, bool) { 28 | var v interface{} 29 | for end := 1; end <= len(src); end++ { 30 | got := t.tree.Get(patricia.Prefix(src[:end])) 31 | if got != nil { 32 | v = got 33 | } 34 | if !t.tree.MatchSubtree(patricia.Prefix(src[:end])) { 35 | break 36 | } 37 | } 38 | return v, v != nil 39 | } 40 | -------------------------------------------------------------------------------- /bench/derekparker_trie.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/derekparker/trie" 5 | ) 6 | 7 | type derekparkerTrie struct { 8 | tree *trie.Trie 9 | } 10 | 11 | func (t *derekparkerTrie) Name() string { 12 | return "github.com/derekparker/trie" 13 | } 14 | 15 | func (t *derekparkerTrie) Build(keys [][]byte, values []interface{}) { 16 | t.tree = trie.New() 17 | for i := range keys { 18 | t.tree.Add(string(keys[i]), values[i]) 19 | } 20 | } 21 | 22 | func (t *derekparkerTrie) Get(s []byte) (interface{}, bool) { 23 | n, ok := t.tree.Find(string(s)) 24 | if !ok { 25 | return nil, false 26 | } 27 | return n.Meta(), true 28 | } 29 | 30 | func (t *derekparkerTrie) LongestPrefix(s []byte) (interface{}, bool) { 31 | // s is assumed contains only ASCII characters. 32 | 33 | var v interface{} 34 | var match bool 35 | for end := 1; end <= len(s); end++ { 36 | if n, ok := t.tree.Find(string(s[:end])); ok { 37 | match = true 38 | v = n.Meta() 39 | } 40 | if !t.tree.HasKeysWithPrefix(string(s[:end])) { 41 | break 42 | } 43 | } 44 | 45 | return v, match 46 | } 47 | -------------------------------------------------------------------------------- /bench/sauerbraten_radix.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/sauerbraten/radix" 7 | ) 8 | 9 | type sauerbratenRadix struct { 10 | tree *radix.Radix 11 | } 12 | 13 | func (t *sauerbratenRadix) Name() string { 14 | return "github.com/sauerbraten/radix" 15 | } 16 | 17 | func (t *sauerbratenRadix) Build(keys [][]byte, values []interface{}) { 18 | t.tree = radix.New() 19 | for i := range keys { 20 | t.tree.Set(string(keys[i]), values[i]) 21 | } 22 | } 23 | 24 | func (t *sauerbratenRadix) LongestPrefix(s []byte) (interface{}, bool) { 25 | // s is assumed contains only ASCII characters. 26 | 27 | var v interface{} 28 | 29 | tt := t.tree 30 | i := 0 31 | for i < len(s) { 32 | tt = tt.SubTreeWithPrefix(string([]byte{s[i]})) 33 | if tt == nil { 34 | break 35 | } 36 | v = tt.Value() 37 | if !strings.HasPrefix(string(s[i:]), tt.Key()) { 38 | break 39 | } 40 | i += len(tt.Key()) 41 | } 42 | 43 | return v, i > 0 44 | } 45 | 46 | func (t *sauerbratenRadix) Get(s []byte) (interface{}, bool) { 47 | v := t.tree.Get(string(s)) 48 | return v, v != nil 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 acomagu 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 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package trie_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/acomagu/trie/v2" 7 | ) 8 | 9 | func Example_match() { 10 | keys := [][]byte{ 11 | []byte("ab"), 12 | []byte("abc"), 13 | []byte("abd"), 14 | } 15 | values := []int{1, 2, 3} // The type of value doesn't have to be int. Can be anything. 16 | t := trie.New(keys, values) 17 | 18 | v, ok := t.Trace([]byte("abc")).Terminal() 19 | fmt.Println(v, ok) // Output: 2 true 20 | } 21 | 22 | func Example_longestPrefix() { 23 | keys := [][]byte{ 24 | []byte("ab"), 25 | []byte("abc"), 26 | []byte("abd"), 27 | } 28 | values := []int{1, 2, 3} // The type of value doesn't have to be int. Can be anything. 29 | t := trie.New(keys, values) 30 | 31 | var v interface{} 32 | var match bool 33 | for _, c := range []byte("abcxxx") { 34 | if t = t.TraceOne(c); t == nil { 35 | break 36 | } 37 | if vv, ok := t.Terminal(); ok { 38 | v = vv 39 | match = true 40 | } 41 | } 42 | 43 | fmt.Println(v, match) // Output: 2 true 44 | } 45 | 46 | func ExampleTree_Terminal() { 47 | keys := [][]byte{[]byte("aa")} 48 | values := []int{1} // The type of value doesn't have to be int. Can be anything. 49 | t := trie.New(keys, values) 50 | 51 | t = t.TraceOne('a') // a 52 | fmt.Println(t.Terminal()) 53 | 54 | t = t.TraceOne('a') // aa 55 | fmt.Println(t.Terminal()) 56 | 57 | t = t.TraceOne('a') // aaa 58 | fmt.Println(t.Terminal()) 59 | 60 | // Output: 61 | // 0 false 62 | // 1 true 63 | // 0 false 64 | } 65 | -------------------------------------------------------------------------------- /bench/go.sum: -------------------------------------------------------------------------------- 1 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 2 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 3 | github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9 h1:aSaTVlEXc2QKl4fzXU1tMYCjlrSc2mA4DZtiVfckQHo= 4 | github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9/go.mod h1:D6ICZm05D9VN1n/8iOtBxLpXtoGp6HDFUJ1RNVieOSE= 5 | github.com/dghubble/trie v0.0.0-20190512033633-6d8e3fa705df h1:WRQekGjYIb3oD1ofBVwBa7+0S+2XtUCefOiFCow9/Cw= 6 | github.com/dghubble/trie v0.0.0-20190512033633-6d8e3fa705df/go.mod h1:P5ymVhkUtwRIkYn2IuBeuVezlrsshMKWQJymph3GOp8= 7 | github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= 8 | github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 9 | github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= 10 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 11 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 12 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 13 | github.com/kkdai/radix v0.0.0-20181128172204-f0c88ccaf15e h1:QGx3SOuz82hIbQYYIY+ZUC3kMcOh6vxXBW9Tr9RmSyM= 14 | github.com/kkdai/radix v0.0.0-20181128172204-f0c88ccaf15e/go.mod h1:pW8/Kep4CBRATIyP+m3fhJN1V0je7kq6H/swWctABh4= 15 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 16 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 17 | github.com/sauerbraten/radix v0.0.0-20150210222551-4445e9cd8982 h1:5uxzTKgljXqGhmr0sjLKfRDuwxDFAVYl0S0SfOx5iGo= 18 | github.com/sauerbraten/radix v0.0.0-20150210222551-4445e9cd8982/go.mod h1:mRvyfb7TVKBjRZcwU4dvd7d4bCKoKykTSO23Rb39uWA= 19 | github.com/tchap/go-patricia v2.3.0+incompatible h1:GkY4dP3cEfEASBPPkWd+AmjYxhmDkqO9/zg7R0lSQRs= 20 | github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= 21 | golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw= 22 | golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 23 | -------------------------------------------------------------------------------- /bench/targets_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/matryer/is" 7 | ) 8 | 9 | func TestTarget_Get(t *testing.T) { 10 | suite := struct { 11 | keys [][]byte 12 | cases []struct { 13 | s []byte 14 | v int 15 | ok bool 16 | } 17 | }{ 18 | keys: [][]byte{ 19 | []byte("AM"), 20 | []byte("AMD"), 21 | []byte("CAD"), 22 | []byte("CAM"), 23 | []byte("CM"), 24 | []byte("DM"), 25 | }, 26 | cases: []struct { 27 | s []byte 28 | v int 29 | ok bool 30 | }{ 31 | { 32 | s: []byte("CAD"), 33 | v: 2, 34 | ok: true, 35 | }, 36 | { 37 | s: []byte("AMM"), 38 | ok: false, 39 | }, 40 | { 41 | s: []byte("D"), 42 | ok: false, 43 | }, 44 | }, 45 | } 46 | 47 | for _, f := range targetFactories { 48 | tg := f() 49 | t.Run(tg.Name(), func(t *testing.T) { 50 | var values []interface{} 51 | for i := range suite.keys { 52 | values = append(values, i) 53 | } 54 | tg.Build(suite.keys, values) 55 | 56 | for _, c := range suite.cases { 57 | t.Run(string(c.s), func(t *testing.T) { 58 | is := is.New(t) 59 | 60 | v, ok := tg.Get(c.s) 61 | is.Equal(ok, c.ok) 62 | if c.ok { 63 | is.Equal(v, c.v) 64 | } 65 | }) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestTarget_LongestPrefix(t *testing.T) { 72 | suite := struct { 73 | keys [][]byte 74 | cases []struct { 75 | s []byte 76 | v int 77 | ok bool 78 | } 79 | }{ 80 | keys: [][]byte{ 81 | []byte("AM"), 82 | []byte("AMD"), 83 | []byte("CAD"), 84 | []byte("CAM"), 85 | []byte("CM"), 86 | []byte("DM"), 87 | }, 88 | cases: []struct { 89 | s []byte 90 | v int 91 | ok bool 92 | }{ 93 | { 94 | s: []byte("CADEER"), 95 | v: 2, 96 | ok: true, 97 | }, 98 | { 99 | s: []byte("AMO"), 100 | v: 0, 101 | ok: true, 102 | }, 103 | { 104 | s: []byte("CAD"), 105 | v: 2, 106 | ok: true, 107 | }, 108 | { 109 | s: []byte("D"), 110 | ok: false, 111 | }, 112 | { 113 | s: []byte("AMDU"), 114 | v: 1, 115 | ok: true, 116 | }, 117 | }, 118 | } 119 | 120 | for _, f := range targetFactories { 121 | tg, ok := f().(targetHasLongestPrefix) 122 | if !ok { 123 | continue 124 | } 125 | 126 | t.Run(tg.Name(), func(t *testing.T) { 127 | var values []interface{} 128 | for i := range suite.keys { 129 | values = append(values, i) 130 | } 131 | tg.Build(suite.keys, values) 132 | 133 | for _, c := range suite.cases { 134 | t.Run(string(c.s), func(t *testing.T) { 135 | is := is.New(t) 136 | 137 | v, ok := tg.LongestPrefix(c.s) 138 | is.Equal(ok, c.ok) 139 | if c.ok { 140 | is.Equal(v, c.v) 141 | } 142 | }) 143 | } 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /bench/bench_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "bufio" 5 | "compress/gzip" 6 | "math/rand" 7 | "os" 8 | "sync" 9 | "testing" 10 | ) 11 | 12 | // "github.com/acomagu/trie" 13 | // "github.com/hashicorp/go-immutable-radix" 14 | // "github.com/armon/go-radix" 15 | // "github.com/gbrlsnchs/radix" 16 | // kkdaiRadix "github.com/kkdai/radix" 17 | // "github.com/tchap/go-patricia/patricia" 18 | 19 | var readData = func() func() ([][]byte, error) { 20 | var once sync.Once 21 | var data [][]byte 22 | 23 | return func() ([][]byte, error) { 24 | var er error 25 | once.Do(func() { 26 | f, err := os.Open("enwiki-latest-all-titles-in-ns0.gz") 27 | if err != nil { 28 | er = err 29 | return 30 | } 31 | gr, err := gzip.NewReader(f) 32 | if err != nil { 33 | er = err 34 | return 35 | } 36 | scn := bufio.NewScanner(gr) 37 | for scn.Scan() { 38 | data = append(data, scn.Bytes()) 39 | } 40 | 41 | // uniq 42 | m := make(map[string]struct{}, len(data)) 43 | for _, d := range data { 44 | m[string(d)] = struct{}{} 45 | } 46 | data = data[:0] 47 | for d := range m { 48 | data = append(data, []byte(d)) 49 | } 50 | }) 51 | 52 | return data, er 53 | } 54 | }() 55 | 56 | func Benchmark(b *testing.B) { 57 | data, err := readData() 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | values := make([]interface{}, 0, len(data)) 63 | for i := range data { 64 | values = append(values, i) 65 | } 66 | 67 | ts := make([]target, len(targetFactories)) 68 | b.Run("Build", func(b *testing.B) { 69 | for it, f := range targetFactories { 70 | b.Run(f().Name(), func(b *testing.B) { 71 | for i := 0; i < b.N; i++ { 72 | ts[it] = f() 73 | ts[it].Build(data, values) 74 | } 75 | }) 76 | } 77 | }) 78 | 79 | b.Run("LongestPrefix", func(b *testing.B) { 80 | var l int 81 | for _, d := range data { 82 | l += len(d) 83 | } 84 | 85 | src := make([]byte, 0, l) 86 | for _, d := range data { 87 | src = append(src, d...) 88 | } 89 | rand.Shuffle(len(src), func(i, j int) { 90 | src[i], src[j] = src[j], src[i] 91 | }) 92 | 93 | for _, t := range ts { 94 | if t, ok := t.(targetHasLongestPrefix); ok { 95 | b.Run(t.Name(), func(b *testing.B) { 96 | begin := 0 97 | for i := 0; i < b.N; i++ { 98 | begin %= len(src) 99 | _, _ = t.LongestPrefix(src[begin:]) 100 | begin++ 101 | } 102 | }) 103 | } 104 | } 105 | }) 106 | 107 | b.Run("Match", func(b *testing.B) { 108 | srcs := make([]int, 0, 10000) 109 | for i := 0; i < 10000; i++ { 110 | srcs = append(srcs, rand.Intn(len(data))) 111 | } 112 | 113 | for _, t := range ts { 114 | b.Run(t.Name(), func(b *testing.B) { 115 | si := 0 116 | for i := 0; i < b.N; i++ { 117 | si %= len(srcs) 118 | _, _ = t.Get(data[srcs[si]]) 119 | si++ 120 | } 121 | }) 122 | } 123 | }) 124 | 125 | // kkdai/radix: Impossible 126 | 127 | // q-radix: Impossible 128 | } 129 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | Go (the standard library) 2 | https://golang.org/ 3 | ---------------------------------------------------------------- 4 | Copyright (c) 2009 The Go Authors. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following disclaimer 14 | in the documentation and/or other materials provided with the 15 | distribution. 16 | * Neither the name of Google Inc. nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ================================================================ 33 | 34 | github.com/matryer/is 35 | https://github.com/matryer/is 36 | ---------------------------------------------------------------- 37 | MIT License 38 | 39 | Copyright (c) 2017-2018 Mat Ryer 40 | 41 | Permission is hereby granted, free of charge, to any person obtaining a copy 42 | of this software and associated documentation files (the "Software"), to deal 43 | in the Software without restriction, including without limitation the rights 44 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 45 | copies of the Software, and to permit persons to whom the Software is 46 | furnished to do so, subject to the following conditions: 47 | 48 | The above copyright notice and this permission notice shall be included in all 49 | copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 52 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 53 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 54 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 55 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 56 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 57 | SOFTWARE. 58 | 59 | ================================================================ 60 | 61 | -------------------------------------------------------------------------------- /trie.go: -------------------------------------------------------------------------------- 1 | package trie 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "golang.org/x/exp/constraints" 8 | ) 9 | 10 | // Algorithm Explanation: https://engineering.linecorp.com/ja/blog/simple-tries/ 11 | 12 | // Tree implements an immutable trie tree. 13 | type Tree[K constraints.Ordered, V any] []node[K, V] 14 | 15 | type node[K constraints.Ordered, V any] struct { 16 | value V 17 | next int 18 | label K 19 | match bool 20 | leaf bool 21 | } 22 | 23 | type kv[K constraints.Ordered, V any] struct { 24 | k []K 25 | v V 26 | } 27 | 28 | // New builds new Tree from keys and values. The len(keys) should equal to 29 | // len(values). 30 | func New[K constraints.Ordered, V any](keys [][]K, values []V) Tree[K, V] { 31 | if len(keys) != len(values) { 32 | panic("length mismatch of keys and values") 33 | } 34 | 35 | kvs := make([]kv[K, V], 0, len(keys)) 36 | for i, k := range keys { 37 | kvs = append(kvs, kv[K, V]{k, values[i]}) 38 | } 39 | sort.Slice(kvs, func(i, j int) bool { 40 | a, b := kvs[i].k, kvs[j].k 41 | for i := 0; i < len(a) && i < len(b); i++ { 42 | if a[i] == b[i] { 43 | continue 44 | } 45 | return a[i] < b[i] 46 | } 47 | if len(a) == len(b) { 48 | panic(fmt.Sprintf("2 same key is passed: %v", kvs[i].k)) 49 | } 50 | return len(a) < len(b) 51 | }) 52 | 53 | t := Tree[K, V]{node[K, V]{next: 1}} 54 | 55 | t = t.construct(kvs, 0, 0) 56 | return t 57 | } 58 | 59 | func (t Tree[K, V]) construct(kvs []kv[K, V], depth, current int) Tree[K, V] { 60 | if depth == len(kvs[0].k) { 61 | t[current].match = true 62 | t[current].value = kvs[0].v 63 | kvs = kvs[1:] 64 | if len(kvs) == 0 { 65 | t[current].leaf = true 66 | return t 67 | } 68 | } 69 | 70 | p := []int{0} 71 | for i := 0; i < len(kvs); { 72 | t = append(t, node[K, V]{ 73 | label: kvs[i].k[depth], 74 | }) 75 | for c := kvs[i].k[depth]; i < len(kvs) && kvs[i].k[depth] == c; i++ { 76 | } 77 | p = append(p, i) 78 | } 79 | 80 | for i := 0; i < len(p)-1; i++ { 81 | t[t.nextOf(current)+i].next = len(t) - t.nextOf(current) - i 82 | t = t.construct(kvs[p[i]:p[i+1]], depth+1, t.nextOf(current)+i) 83 | } 84 | return t 85 | } 86 | 87 | // Trace returns the subtree of t whose root is the node traced from the root 88 | // of t by path. It doesn't modify t itself, but returns the subtree. 89 | func (t Tree[K, V]) Trace(path []K) Tree[K, V] { 90 | if t == nil { 91 | return nil 92 | } 93 | 94 | var u int 95 | for _, c := range path { 96 | if t[u].leaf { 97 | return nil 98 | } 99 | u = t.nextOf(u) 100 | v := t.nextOf(u) 101 | if v-u > 40 { 102 | // Binary Search 103 | u += sort.Search(v-u, func(m int) bool { 104 | return t[u+m].label >= c 105 | }) 106 | } else { 107 | // Linear Search 108 | for ; u != v-1 && t[u].label < c; u++ { 109 | } 110 | } 111 | if u > v || t[u].label != c { 112 | return nil 113 | } 114 | } 115 | return t[u:] 116 | } 117 | 118 | // TraceOne is shorthand for Trace([]K{c}). 119 | func (t Tree[K, V]) TraceOne(c K) Tree[K, V] { 120 | return t.Trace([]K{c}) 121 | } 122 | 123 | // Terminal returns the value of the root of t. The second return value 124 | // indicates whether the node has a value; if it is false, the first return 125 | // value is zero value. 126 | func (t Tree[K, V]) Terminal() (V, bool) { 127 | var zero V 128 | if len(t) == 0 { 129 | return zero, false 130 | } 131 | return t[0].value, t[0].match 132 | } 133 | 134 | // Predict returns the all values in the tree, t. The complexity is proportional 135 | // to the number of nodes in t(it's not equal to len(t)). 136 | func (t Tree[K, V]) Predict() []V { 137 | if len(t) == 0 || t[0].leaf { 138 | return nil 139 | } 140 | 141 | // Search linearly all of the child. 142 | var end int 143 | for !t[end].leaf { 144 | end = t.nextOf(t.nextOf(end)) - 1 145 | } 146 | 147 | var values []V 148 | for i := t.nextOf(0); i <= end; i++ { 149 | if t[i].match { 150 | values = append(values, t[i].value) 151 | } 152 | } 153 | return values 154 | } 155 | 156 | // Children returns the bytes of all direct children of the root of t. The result 157 | // is sorted in ascending order. 158 | func (t Tree[K, V]) Children() []K { 159 | if len(t) == 0 || t[0].leaf { 160 | return nil 161 | } 162 | 163 | var children []K 164 | for _, c := range t[t.nextOf(0):t.nextOf(t.nextOf(0))] { 165 | children = append(children, c.label) 166 | } 167 | return children 168 | } 169 | 170 | // nextOf returns the index of the next node of t[i]. 171 | func (t Tree[K, V]) nextOf(i int) int { 172 | return i + t[i].next 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trie: The fast and flexible Trie Tree implementation 2 | 3 | [![Reference](https://pkg.go.dev/badge/github.com/acomagu/trie/v2)](https://pkg.go.dev/github.com/acomagu/trie/v2) 4 | 5 | The [Trie Tree](https://wikipedia.org/wiki/Trie) implementation in Go. It has flexible interface and works fast as Radix Tree implementation. 6 | 7 | This is basically an implementation of the algorithm described in [簡単なトライ - LINE ENGINEERING](https://engineering.linecorp.com/ja/blog/simple-tries/). I really appreciate the amazing ideas and the clear and easy-to-understand explanation. 8 | 9 | ## Import Path 10 | 11 | github.com/acomagu/trie/v2 12 | 13 | ## Benchmark 14 | 15 | Run `make bench` to run it locally. [The latest benchmark result is here.](https://github.com/acomagu/trie/actions/workflows/bench.yml) 16 | 17 | ### Exact Matching 18 | 19 | The task is to determine whether a string matches one of all Wikipedia titles. 20 | 21 | | Package | Time | Objects Allocated | 22 | | ---------------------------- | -------------------:| --------------------:| 23 | | **acomagu/trie** | **1090 ns/op** | **0 allocs/op** | 24 | | sauerbraten/radix | 2445 ns/op | 0 allocs/op | 25 | | dghubble/trie | 2576 ns/op | 0 allocs/op | 26 | | hashicorp/go-immutable-radix | 3660 ns/op | 0 allocs/op | 27 | | derekparker/trie | 4010 ns/op | 0 allocs/op | 28 | | armon/go-radix | 11745 ns/op | 0 allocs/op | 29 | | kkdai/radix | 18809 ns/op | 0 allocs/op | 30 | | tchap/go-patricia/patricia | 21498 ns/op | 0 allocs/op | 31 | 32 | ### Longest Prefix 33 | 34 | The task is to answer which of all Wikipedia titles can be the longest prefix of a string. 35 | 36 | | Package | Time | Objects Allocated | 37 | | ---------------------------- | -------------------:| --------------------:| 38 | | **acomagu/trie** | **140 ns/op** | **0 allocs/op** | 39 | | hashicorp/go-immutable-radix | 159 ns/op | 0 allocs/op | 40 | | tchap/go-patricia/patricia | 252 ns/op | 0 allocs/op | 41 | | derekparker/trie | 2374 ns/op | 0 allocs/op | 42 | | sauerbraten/radix | 3264938 ns/op | 0 allocs/op | 43 | | armon/go-radix | 22129827 ns/op | 1 allocs/op | 44 | 45 | (dghubble/trie and kkdai/radix don't have way to do.) 46 | 47 | ### Build 48 | 49 | The task is to prepare Trie/Radix Tree with all of the Wikipedia titles. 50 | 51 | | Package | Time | Objects Allocated | 52 | | ---------------------------- | -------------------:| --------------------:| 53 | | sauerbraten/radix | 118959250 ns/op | 408564 allocs/op | 54 | | **acomagu/trie** | **542902000 ns/op** | **421906 allocs/op** | 55 | | dghubble/trie | 609406300 ns/op | 1136281 allocs/op | 56 | | derekparker/trie | 1046705400 ns/op | 1801539 allocs/op | 57 | | armon/go-radix | 1750312500 ns/op | 1446050 allocs/op | 58 | | kkdai/radix | 2280362300 ns/op | 1742841 allocs/op | 59 | | tchap/go-patricia/patricia | 2898335700 ns/op | 1150947 allocs/op | 60 | | hashicorp/go-immutable-radix | 7614342400 ns/op | 45097986 allocs/op | 61 | 62 | ## Examples 63 | 64 | The common preparation for each example: 65 | 66 | ```Go 67 | import ( 68 | "fmt" 69 | 70 | "github.com/acomagu/trie/v2" 71 | ) 72 | 73 | keys := [][]byte{ 74 | []byte("ab"), 75 | []byte("abc"), 76 | []byte("abd"), 77 | } 78 | values := []int{1, 2, 3} // The type of value doesn't have to be int. Can be anything. 79 | t := trie.New(keys, values) 80 | ``` 81 | 82 | `New()` takes keys and values as the arguments. `values[i]` is the *value* of the corresponding key, `keys[i]`. 83 | 84 | ### Exact Match 85 | 86 | ```Go 87 | v, ok := t.Trace([]byte("abc")).Terminal() 88 | fmt.Println(v, ok) // => 2 true 89 | ``` 90 | 91 | [Playground](https://go.dev/play/p/dqswqXlxe3Q) 92 | 93 | ### Longest Prefix 94 | 95 | ```Go 96 | var v int 97 | var match bool 98 | for _, c := range []byte("abcxxx") { 99 | if t = t.TraceOne(c); t == nil { 100 | break 101 | } 102 | if vv, ok := t.Terminal(); ok { 103 | v = vv 104 | match = true 105 | } 106 | } 107 | 108 | fmt.Println(v, match) // => 2 true 109 | ``` 110 | 111 | [Playground](https://go.dev/play/p/wvZmeIXaYAl) 112 | 113 | No special function to get longest prefix because it can be implemented yourself easily using the existing methods. 114 | -------------------------------------------------------------------------------- /trie_test.go: -------------------------------------------------------------------------------- 1 | package trie 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/matryer/is" 8 | ) 9 | 10 | func TestTrace(t *testing.T) { 11 | is := is.New(t) 12 | 13 | type sc struct { 14 | path [][]byte 15 | nl bool 16 | } 17 | cases := []struct { 18 | ss [][]byte 19 | scs []sc 20 | }{ 21 | { 22 | ss: [][]byte{ 23 | []byte("AM"), 24 | []byte("AMD"), 25 | []byte("CAD"), 26 | []byte("CAM"), 27 | []byte("CM"), 28 | []byte("DM"), 29 | }, 30 | scs: []sc{ 31 | {[][]byte{[]byte("A")}, false}, 32 | {[][]byte{[]byte("AM")}, false}, 33 | {[][]byte{[]byte("AMC")}, true}, 34 | }, 35 | }, 36 | { 37 | ss: [][]byte{ 38 | []byte("12a"), 39 | }, 40 | scs: []sc{ 41 | {[][]byte{[]byte("1")}, false}, 42 | {[][]byte{[]byte("12")}, false}, 43 | {[][]byte{[]byte("123")}, true}, 44 | }, 45 | }, 46 | } 47 | 48 | for i, c := range cases { 49 | t.Run(fmt.Sprint(i), func(t *testing.T) { 50 | values := make([]interface{}, len(c.ss)) 51 | for _, sc := range c.scs { 52 | tt := New(c.ss, values) 53 | for _, p := range sc.path { 54 | tt = tt.Trace(p) 55 | } 56 | is.Equal(tt == nil, sc.nl) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestMatch(t *testing.T) { 63 | is := is.New(t) 64 | 65 | type entry struct { 66 | ss [][]byte 67 | complete bool 68 | } 69 | cases := []struct { 70 | ss [][]byte 71 | es []entry 72 | }{ 73 | { 74 | ss: [][]byte{ 75 | []byte("AM"), 76 | []byte("AMD"), 77 | []byte("CAD"), 78 | []byte("CAM"), 79 | []byte("CM"), 80 | []byte("DM"), 81 | }, 82 | es: []entry{ 83 | { 84 | ss: [][]byte{ 85 | []byte("AM"), 86 | }, 87 | complete: true, 88 | }, 89 | { 90 | ss: [][]byte{[]byte("AM")}, 91 | complete: true, 92 | }, 93 | { 94 | ss: [][]byte{[]byte("A")}, 95 | complete: false, 96 | }, 97 | }, 98 | }, 99 | { 100 | ss: [][]byte{ 101 | []byte("xxx"), 102 | []byte("abcd"), 103 | []byte("ab"), 104 | []byte("abcc"), 105 | []byte("abc"), 106 | []byte("xxxy"), 107 | }, 108 | es: []entry{ 109 | { 110 | ss: nil, 111 | complete: false, 112 | }, 113 | { 114 | ss: [][]byte{[]byte("a")}, 115 | complete: false, 116 | }, 117 | { 118 | ss: [][]byte{[]byte("ab")}, 119 | complete: true, 120 | }, 121 | { 122 | ss: [][]byte{[]byte("abc")}, 123 | complete: true, 124 | }, 125 | { 126 | ss: [][]byte{[]byte("abce")}, 127 | complete: false, 128 | }, 129 | }, 130 | }, 131 | { 132 | ss: [][]byte{ 133 | nil, 134 | }, 135 | es: []entry{ 136 | { 137 | ss: [][]byte{nil}, 138 | complete: true, 139 | }, 140 | { 141 | ss: [][]byte{[]byte("a")}, 142 | complete: false, 143 | }, 144 | }, 145 | }, 146 | } 147 | 148 | for i, c := range cases { 149 | t.Run(fmt.Sprint(i), func(t *testing.T) { 150 | var values []interface{} 151 | for i := range c.ss { 152 | values = append(values, i) 153 | } 154 | for j, e := range c.es { 155 | t.Run(fmt.Sprint(j), func(t *testing.T) { 156 | tt := New(c.ss, values) 157 | for _, s := range e.ss { 158 | tt = tt.Trace(s) 159 | } 160 | _, ok := tt.Terminal() 161 | is.Equal(ok, e.complete) 162 | }) 163 | } 164 | }) 165 | } 166 | } 167 | 168 | func TestPredict(t *testing.T) { 169 | is := is.New(t) 170 | 171 | type entry struct { 172 | ss [][]byte 173 | predict []int 174 | } 175 | cases := []struct { 176 | ss [][]byte 177 | es []entry 178 | }{ 179 | { 180 | ss: [][]byte{ 181 | []byte("AA"), 182 | []byte("AB"), 183 | []byte("BC"), 184 | }, 185 | es: []entry{ 186 | { 187 | ss: [][]byte{[]byte("A")}, 188 | predict: []int{0, 1}, 189 | }, 190 | { 191 | ss: [][]byte{[]byte("AA")}, 192 | predict: []int{}, 193 | }, 194 | { 195 | ss: [][]byte{[]byte("B")}, 196 | predict: []int{2}, 197 | }, 198 | }, 199 | }, 200 | { 201 | ss: [][]byte{[]byte("aaa")}, 202 | es: []entry{ 203 | { 204 | ss: [][]byte{[]byte("a")}, 205 | predict: []int{0}, 206 | }, 207 | }, 208 | }, 209 | } 210 | 211 | for i, c := range cases { 212 | t.Run(fmt.Sprint(i), func(t *testing.T) { 213 | var vs []interface{} 214 | for is := range c.ss { 215 | vs = append(vs, is) 216 | } 217 | for j, e := range c.es { 218 | t.Run(fmt.Sprint(j), func(t *testing.T) { 219 | tt := New(c.ss, vs) 220 | for _, s := range e.ss { 221 | tt = tt.Trace(s) 222 | } 223 | predict := tt.Predict() 224 | t.Log(predict, "==", e.predict) 225 | is.Equal(len(predict), len(e.predict)) 226 | for i := range predict { 227 | is.Equal(predict[i], e.predict[i]) 228 | } 229 | }) 230 | } 231 | }) 232 | } 233 | } 234 | 235 | func TestChildren(t *testing.T) { 236 | is := is.New(t) 237 | 238 | type entry struct { 239 | s []byte 240 | children []byte 241 | } 242 | cases := []struct { 243 | ss [][]byte 244 | es []entry 245 | }{ 246 | { 247 | ss: [][]byte{ 248 | []byte("AA"), 249 | []byte("AB"), 250 | []byte("ABC"), 251 | []byte("BC"), 252 | []byte("BDE"), 253 | []byte("C"), 254 | []byte("CC"), 255 | }, 256 | es: []entry{ 257 | { 258 | s: []byte("A"), 259 | children: []byte{'A', 'B'}, 260 | }, 261 | { 262 | s: []byte("AA"), 263 | children: []byte{}, 264 | }, 265 | { 266 | s: []byte("B"), 267 | children: []byte{'C', 'D'}, 268 | }, 269 | { 270 | s: []byte("C"), 271 | children: []byte{'C'}, 272 | }, 273 | { 274 | s: nil, 275 | children: []byte{'A', 'B', 'C'}, 276 | }, 277 | }, 278 | }, 279 | } 280 | 281 | for i, c := range cases { 282 | t.Run(fmt.Sprint(i), func(t *testing.T) { 283 | var vs []interface{} 284 | for is := range c.ss { 285 | vs = append(vs, is) 286 | } 287 | for j, e := range c.es { 288 | t.Run(fmt.Sprint(j), func(t *testing.T) { 289 | children := New(c.ss, vs).Trace(e.s).Children() 290 | t.Log(string(children), "==", string(e.children)) 291 | is.Equal(len(children), len(e.children)) 292 | for i := range children { 293 | is.Equal(children[i], e.children[i]) 294 | } 295 | }) 296 | } 297 | }) 298 | } 299 | } 300 | --------------------------------------------------------------------------------