├── .gitignore ├── lib ├── debug.js ├── NoopIterator.js ├── TrieIterator.js └── HashMapNode.js ├── .npmignore ├── countChildrenMapInstances.js ├── package.json ├── bench.md ├── LICENSE ├── benchmark.js ├── test ├── TrieIterator.test.js ├── HashMapNode.test.js └── Trie.test.js ├── old.js ├── old_test.js ├── README.md ├── lorem.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | .DS_Store 5 | *.log 6 | 1.js -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug') 2 | 3 | module.exports = name => debug(`digital-tree:${name}`) 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | old.js 4 | old_test.js 5 | node_modules 6 | .DS_Store 7 | *.log 8 | .git 9 | 1.js 10 | -------------------------------------------------------------------------------- /lib/NoopIterator.js: -------------------------------------------------------------------------------- 1 | class NoopIterator { 2 | next() { 3 | return this 4 | } 5 | 6 | get value() {} 7 | get done() { return true } 8 | 9 | [Symbol.iterator]() { 10 | return this 11 | } 12 | } 13 | 14 | module.exports = NoopIterator -------------------------------------------------------------------------------- /countChildrenMapInstances.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | this program adds the benchmark lorem to a trie 4 | and prints the stats about creation of map instances 5 | for the children collection of HashMapNode 6 | 7 | */ 8 | const debug = require('./lib/debug')('countChildrenMapInstances') 9 | 10 | if (!debug.enabled) { 11 | console.error(`must enable debug for this program: 12 | env DEBUG=digital-tree\* node countChildrenMapInstances.js`) 13 | process.exit(1) 14 | } 15 | 16 | const lorem = require('./lorem') 17 | const Trie = require('./index') 18 | const HashMapNode = require('./lib/HashMapNode') 19 | 20 | const tree = Trie.create() 21 | 22 | for (let word of lorem) { 23 | tree.put(word, 1) 24 | } 25 | 26 | console.log('maps created if creation was in Ctor (not lazy)', HashMapNode.ctor) 27 | console.log('maps created lazily', HashMapNode.lazy) 28 | console.log('calls to add()', HashMapNode.add) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digital-tree", 3 | "version": "2.0.4", 4 | "description": "trie data structure", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npx ava", 8 | "bench": "node benchmark.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/kessler/node-digital-tree" 13 | }, 14 | "keywords": [ 15 | "trie", 16 | "autocomplete" 17 | ], 18 | "author": "Yaniv Kessler", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/kessler/node-digital-tree/issues" 22 | }, 23 | "homepage": "https://github.com/kessler/node-digital-tree", 24 | "dependencies": { 25 | "benchmark": "^2.1.4", 26 | "debug": "^4.1.1", 27 | "json-stringify-safe": "^5.0.1" 28 | }, 29 | "devDependencies": { 30 | "ava": "^3.15.0", 31 | "benchmarkify": "^2.1.1", 32 | "trie-d": "^1.0.6" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bench.md: -------------------------------------------------------------------------------- 1 | # digital-tree benchmark 2 | 3 | ## Platform info: 4 | 5 | Darwin 16.7.0 x64 6 | Node.JS: 12.4.0 7 | V8: 7.4.288.27-node.18 8 | Intel(R) Core(TM) i5-5287U CPU @ 2.90GHz × 4 9 | 10 | ## Suite: Trie put 11 | ✔ current implementation 5,585 rps 12 | 13 | ✔ old implementation 4,910 rps 14 | 15 | - current implementation 0% (5,585 rps) (avg: 179μs) 16 | - old implementation -12.08% (4,910 rps) (avg: 203μs) 17 | ----------------------------------------------------------------------- 18 | 19 | ## Suite: Trie get 20 | ✔ current implementation 5,449 rps 21 | 22 | ✔ old implementation 5,214 rps 23 | 24 | - current implementation 0% (5,449 rps) (avg: 183μs) 25 | - old implementation -4.32% (5,214 rps) (avg: 191μs) 26 | ----------------------------------------------------------------------- -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Yaniv Kessler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | const Benchmarkify = require('benchmarkify') 2 | const lorem = require('./lorem') 3 | const Trie = require('./index') 4 | const OldTrie = require('./old') 5 | const TrieD = require('trie-d') 6 | 7 | const benchmark = new Benchmarkify('digital-tree benchmark').printHeader() 8 | const putBench = benchmark.createSuite('Trie put/add') 9 | const putAllBench = benchmark.createSuite('Trie put/add all') 10 | const getBench = benchmark.createSuite('Trie get') 11 | const searchBench = benchmark.createSuite('Trie search by prefix') 12 | 13 | const testSubjects = [{ 14 | create: () => Trie.create(), 15 | name: 'current implementation', 16 | put: (trie, key, value) => trie.put(key, value), 17 | putAll: (trie, keys) => trie.putAll(keys), 18 | get: (trie, key) => trie.get(key), 19 | search: (trie, key) => trie.search(key) 20 | }, 21 | { 22 | create: () => new OldTrie(), 23 | name: 'old implementation', 24 | put: (trie, key, value) => trie.put(key, value), 25 | get: (trie, key) => trie.get(key), 26 | search: (trie, key) => trie.searchByPrefix(key) 27 | }, 28 | { 29 | create: () => new TrieD(), 30 | name: 'trie-d', 31 | put: (trie, key, value) => trie.add(key), 32 | putAll: (trie, keys) => trie.addAll(keys) 33 | } 34 | ] 35 | 36 | for (let { name, create, put, putAll, get, search } of testSubjects) { 37 | const instance = create() 38 | 39 | if (put) { 40 | putBench.add(name, () => { 41 | for (let word of lorem) { 42 | put(instance, word, 1) 43 | } 44 | }) 45 | } 46 | 47 | if (putAll) { 48 | putAllBench.add(name, () => { 49 | putAll(instance, lorem) 50 | }) 51 | } 52 | 53 | if (get) { 54 | getBench.add(name, () => { 55 | for (let word of lorem) { 56 | get(instance, word) 57 | } 58 | }) 59 | } 60 | 61 | if (search) { 62 | searchBench.add(name, () => { 63 | for (let word of lorem) { 64 | for (let i = 1; i <= word.length; i++) { 65 | let part = word.substr(0, i) 66 | let result = search(instance, part) 67 | } 68 | } 69 | }) 70 | } 71 | } 72 | 73 | async function main() { 74 | await putBench.run() 75 | await putAllBench.run() 76 | // await getBench.run() 77 | // await searchBench.run() 78 | } 79 | 80 | main() -------------------------------------------------------------------------------- /test/TrieIterator.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const TrieIterator = require('../lib/TrieIterator') 3 | 4 | test('TrieIterator DFS with keys', t => { 5 | const { root } = createTree() 6 | 7 | const iterator = new TrieIterator(root, { memory: TrieIterator.Stack }) 8 | const iteration = Array.from(iterator) 9 | 10 | t.deepEqual(iteration, [ 11 | [ 12 | ['a'], '456' 13 | ], 14 | [ 15 | ['a', '3'], '4563' 16 | ], 17 | [ 18 | ['a', '2'], '4562' 19 | ], 20 | [ 21 | ['a', '2', 'a'], '45621' 22 | ], 23 | [ 24 | ['a', '1'], '4561' 25 | ], 26 | [ 27 | ['a', '1', 'a'], '45611' 28 | ], 29 | [ 30 | ['a', '1', 'a', '1'], '456111' 31 | ] 32 | ]) 33 | }) 34 | 35 | test('TrieIterator DFS values only', t => { 36 | const { root } = createTree() 37 | 38 | const iterator = new TrieIterator(root, { memory: TrieIterator.Stack, includeKeys: false }) 39 | const iteration = Array.from(iterator) 40 | 41 | t.deepEqual(iteration, ['456', '4563', '4562', '45621', '4561', '45611', '456111']) 42 | }) 43 | 44 | test('TrieIterator BFS with keys', t => { 45 | const { root } = createTree() 46 | 47 | const iterator = new TrieIterator(root, { memory: TrieIterator.Queue }) 48 | const iteration = Array.from(iterator) 49 | 50 | t.deepEqual(iteration, [ 51 | [ 52 | ['a'], '456' 53 | ], 54 | [ 55 | ['a', '1'], '4561' 56 | ], 57 | [ 58 | ['a', '2'], '4562' 59 | ], 60 | [ 61 | ['a', '3'], '4563' 62 | ], 63 | [ 64 | ['a', '1', 'a'], '45611' 65 | ], 66 | [ 67 | ['a', '2', 'a'], '45621' 68 | ], 69 | [ 70 | ['a', '1', 'a', '1'], '456111' 71 | ] 72 | ]) 73 | }) 74 | 75 | test('TrieIterator BFS values only', t => { 76 | const { root } = createTree() 77 | 78 | const iterator = new TrieIterator(root, { memory: TrieIterator.Queue, includeKeys: false }) 79 | const iteration = Array.from(iterator) 80 | 81 | t.deepEqual(iteration, ['456', '4561', '4562', '4563', '45611', '45621', '456111']) 82 | }) 83 | 84 | /* 85 | root 86 | c1: a => 456 87 | c11: 1 => 4561 88 | c111: a => 45611 89 | c1111: 1 => 456111 90 | c12: 2 => 4562 91 | c121: a => 4561 92 | c13: 3 => 4563 93 | */ 94 | function createTree() { 95 | const root = new Map() 96 | 97 | const c1 = new Map() 98 | const c11 = new Map() 99 | const c111 = new Map() 100 | const c1111 = new Map() 101 | const c12 = new Map() 102 | const c121 = new Map() 103 | const c13 = new Map() 104 | 105 | c1.value = '456' 106 | c11.value = '4561' 107 | c111.value = '45611' 108 | c1111.value = '456111' 109 | c12.value = '4562' 110 | c121.value = '45621' 111 | c13.value = '4563' 112 | 113 | root.set('a', c1) 114 | c1.set('1', c11) 115 | c1.set('2', c12) 116 | c12.set('a', c121) 117 | c1.set('3', c13) 118 | c11.set('a', c111) 119 | c111.set('1', c1111) 120 | 121 | return { root, c1, c11, c12, c13, c111, c1111 } 122 | } -------------------------------------------------------------------------------- /old.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('digital-tree') 2 | 3 | module.exports = Trie 4 | 5 | function Trie() { 6 | this._data = {} 7 | } 8 | 9 | /** 10 | * put something in the tree. 11 | * 12 | * @param {Array} key - each member in the array is a level in the tree 13 | * @param {variant} value - the value to store 14 | * 15 | */ 16 | Trie.prototype.put = function(key, value) { 17 | debug('put( {%s}, {%s} )', key, value) 18 | var current = this._data 19 | 20 | for (var i = 0; i < key.length; i++) { 21 | var node = key[i] 22 | 23 | if (current[node] === undefined) 24 | current[node] = {} 25 | 26 | current = current[node] 27 | } 28 | 29 | current.$ = value 30 | } 31 | 32 | /** 33 | * remove something from the tree 34 | * 35 | * @param {Array} key 36 | * 37 | * @return {Object} subtree that was removed 38 | */ 39 | Trie.prototype.remove = function(key) { 40 | debug('remove( {%s} )', key) 41 | 42 | var current = this._data 43 | var parent 44 | 45 | // find the path, return nothing if its not there 46 | for (var i = 0; i < key.length; i++) { 47 | var node = key[i] 48 | 49 | if (current[node] === undefined) 50 | return 51 | 52 | parent = current 53 | current = current[node] 54 | } 55 | 56 | var last = key[key.length - 1] 57 | var subtree = parent[last] 58 | 59 | if (parent) { 60 | delete parent[last] 61 | } 62 | 63 | return subtree 64 | } 65 | 66 | /** 67 | * get something from the tree 68 | * 69 | * @param {Array} key 70 | * 71 | * @return {variant} the value that was placed under that key 72 | */ 73 | Trie.prototype.get = function(key) { 74 | debug('get( {%s} )', key) 75 | var current = this._data 76 | 77 | for (var i = 0; i < key.length; i++) { 78 | var node = key[i] 79 | 80 | if (current[node] === undefined) 81 | return undefined 82 | 83 | current = current[node] 84 | } 85 | 86 | return current.$ 87 | } 88 | 89 | /** 90 | * Search for something in the tree 91 | * 92 | * @param key an array of tokens 93 | * @param excludeKeys if true result will only include the leaves and not the whole path 94 | * 95 | * @return {Array} an array of arrays, each sub array contains the key and the value 96 | */ 97 | Trie.prototype.searchByPrefix = function(key, excludeKeys) { 98 | debug('search( {%s} )', key) 99 | 100 | var results = [] 101 | 102 | var current = this._data 103 | 104 | for (var i = 0; i < key.length; i++) { 105 | var node = key[i] 106 | 107 | if (current[node] === undefined) 108 | return results 109 | 110 | current = current[node] 111 | } 112 | 113 | this._searchByPrefixCollect(key, current, results, excludeKeys) 114 | 115 | return results 116 | } 117 | 118 | Trie.prototype._searchByPrefixCollect = function(path, parent, results, excludeKeys) { 119 | if (!parent) return; 120 | 121 | for (var k in parent) { 122 | if (k === '$') { 123 | 124 | if (excludeKeys) 125 | results.push(parent.$) 126 | else 127 | results.push([path.concat([]), parent.$]) 128 | 129 | continue 130 | } 131 | 132 | var current = parent[k] 133 | 134 | this._searchByPrefixCollect(path.concat([k]), current, results, excludeKeys) 135 | } 136 | } -------------------------------------------------------------------------------- /old_test.js: -------------------------------------------------------------------------------- 1 | var Trie = require('./index.js') 2 | var assert = require('assert') 3 | var lorem = require('./lorem.js') 4 | 5 | console.log('lorem ipsum has %s words', lorem.length) 6 | 7 | describe('Trie', function () { 8 | 9 | var trie 10 | 11 | it('put()', function () { 12 | assert.strictEqual(typeof trie._data.a, 'object') 13 | assert.strictEqual(typeof trie._data.a.b, 'object') 14 | assert.strictEqual(typeof trie._data.a.b.c, 'object') 15 | assert.strictEqual(typeof trie._data.a.b.d, 'object') 16 | assert.strictEqual(trie._data.a.b.c.$, 'data') 17 | assert.strictEqual(trie._data.a.b.d.$, 'data1') 18 | }) 19 | 20 | it('remove()', function () { 21 | 22 | var subtree = trie.remove(['a', 'b', 'c']) 23 | 24 | assert.strictEqual(trie._data.a.b.c, undefined) 25 | assert.strictEqual(subtree.$, 'data') 26 | }) 27 | 28 | it('get()', function () { 29 | assert.strictEqual(trie.get(['a', 'b', 'c']), 'data') 30 | }) 31 | 32 | it('doesnt get()', function () { 33 | assert.strictEqual(trie.get(['a', 'b']), undefined) 34 | }) 35 | 36 | it('_collect()', function () { 37 | var results = [] 38 | var path = ['a', 'b'] 39 | trie._searchByPrefixCollect(path, trie._data['a']['b'], results) 40 | 41 | assert.deepEqual(results[0], [['a', 'b', 'c'], 'data']) 42 | assert.deepEqual(results[1], [['a', 'b', 'd'], 'data1']) 43 | }) 44 | 45 | it('can be searched', function () { 46 | var results = trie.searchByPrefix(['a', 'b']) 47 | assert.deepEqual(results[0], [['a', 'b', 'c'], 'data']) 48 | assert.deepEqual(results[1], [['a', 'b', 'd'], 'data1']) 49 | }) 50 | 51 | it('can be searched and results will not include keys, just data', function () { 52 | var results = trie.searchByPrefix(['a', 'b'], true) 53 | 54 | assert.deepEqual(results[0], 'data') 55 | assert.deepEqual(results[1], 'data1') 56 | }) 57 | 58 | it('wont return results if prefix does not exist', function () { 59 | var results = trie.searchByPrefix(['a', 'l']) 60 | 61 | assert.strictEqual(results.length, 0) 62 | }) 63 | 64 | describe('benchmark', function () { 65 | var putBench 66 | var searchBench 67 | 68 | beforeEach(function () { 69 | putBench = new Trie() 70 | searchBench = new Trie() 71 | 72 | // create the search bench trie outside the test time 73 | for (var i = 0; i < lorem.length; i++) { 74 | searchBench.put(lorem[i], 1) 75 | } 76 | }) 77 | 78 | it('put', function () { 79 | this.slow(5) 80 | 81 | for (var i = 0; i < lorem.length; i++) { 82 | putBench.put(lorem[i], 1) 83 | } 84 | }) 85 | 86 | it('normal search', function () { 87 | this.slow(5) 88 | for (var i = 0; i < lorem.length; i++) { 89 | searchBench.searchByPrefix(lorem[i]) 90 | } 91 | }) 92 | 93 | it('exclude key search', function () { 94 | this.slow(3) 95 | for (var i = 0; i < lorem.length; i++) { 96 | searchBench.searchByPrefix(lorem[i], true) 97 | } 98 | }) 99 | }) 100 | 101 | beforeEach(function () { 102 | trie = new Trie() 103 | trie.put(['a', 'b', 'c'], 'data') 104 | trie.put(['a', 'b', 'd'], 'data1') 105 | trie.put(['a'], 'data2') 106 | trie.put(['a', 'd'], 'data3') 107 | }) 108 | }) -------------------------------------------------------------------------------- /lib/TrieIterator.js: -------------------------------------------------------------------------------- 1 | const debug = require('./debug')('TrieIterator') 2 | 3 | class TrieIterator { 4 | 5 | /** 6 | * 7 | * @param {HashMapNode} root 8 | * @param {Stack|Queue} options.memory 9 | * @param {Boolean} [options.includeKeys=true] include keys in the iteration 10 | * @param {Array} options.prefix attach a prefix to all the keys 11 | * 12 | */ 13 | constructor(root, { memory, includeKeys = true, prefix }) { 14 | this._done = false 15 | this._memory = new memory() 16 | this._root = root 17 | 18 | let key 19 | 20 | if (includeKeys) { 21 | 22 | this._value = this._valueWithKey 23 | this._addChildrenToMemory = this._addChildrenToMemoryIncludeKeys 24 | key = toKey(prefix) 25 | 26 | debug('includeKeys', key) 27 | } else { 28 | debug('excludeKeys') 29 | this._value = this._valueWithoutKey 30 | this._addChildrenToMemory = this._addChildrenToMemoryExcludeKeys 31 | } 32 | 33 | this._memory.add({ key, node: root }) 34 | } 35 | 36 | next() { 37 | const current = this._memory.next() 38 | 39 | // we're done 40 | if (!current) { 41 | debug('done') 42 | this._done = true 43 | return this 44 | } 45 | 46 | const { node: currentNode, key: currentKey } = current 47 | debug(currentNode) 48 | 49 | // defined in the Ctor based on withKeys state 50 | this._addChildrenToMemory(this._memory, currentNode, currentKey) 51 | 52 | // proceed to next if there's no value 53 | if (!currentNode.value) { 54 | debug('no value') 55 | return this.next() 56 | } 57 | 58 | this._current = current 59 | 60 | return this 61 | } 62 | 63 | _addChildrenToMemoryIncludeKeys(memory, currentNode, currentKey) { 64 | debug('_addChildrenToMemoryIncludeKeys') 65 | 66 | for (let [key, childNode] of currentNode.entries()) { 67 | this._memory.add({ key: currentKey.concat([key]), node: childNode }) 68 | } 69 | } 70 | 71 | _addChildrenToMemoryExcludeKeys(memory, currentNode, currentKey) { 72 | debug('_addChildrenToMemoryExcludeKeys') 73 | 74 | for (let childNode of currentNode.values()) { 75 | this._memory.add({ key: undefined, node: childNode }) 76 | } 77 | } 78 | 79 | get done() { 80 | return this._done 81 | } 82 | 83 | get value() { 84 | // defined in the Ctor based on withKeys state 85 | return this._value() 86 | } 87 | 88 | _valueWithKey() { 89 | const { key, node } = this._current 90 | return [key, node.value] 91 | } 92 | 93 | _valueWithoutKey() { 94 | const { node } = this._current 95 | return node.value 96 | } 97 | 98 | [Symbol.iterator]() { 99 | return this 100 | } 101 | } 102 | 103 | function toKey(prefix) { 104 | if (prefix) { 105 | return Array.from(prefix) 106 | } 107 | 108 | return [] 109 | } 110 | 111 | class Stack { 112 | constructor() { 113 | this._arr = [] 114 | } 115 | 116 | add(something) { 117 | this._arr.push(something) 118 | } 119 | 120 | next() { 121 | return this._arr.pop() 122 | } 123 | } 124 | 125 | class Queue { 126 | constructor() {} 127 | 128 | add(something) { 129 | const next = { something } 130 | 131 | if (!this._head) { 132 | this._head = next 133 | this._tail = next 134 | return 135 | } 136 | 137 | this._tail.next = next 138 | this._tail = next 139 | } 140 | 141 | next() { 142 | if (this._head) { 143 | const next = this._head 144 | this._head = this._head.next 145 | return next.something 146 | } 147 | } 148 | } 149 | 150 | TrieIterator.Queue = Queue 151 | TrieIterator.Stack = Stack 152 | 153 | module.exports = TrieIterator -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # digital tree 2 | 3 | A trie data structure implementation. 4 | 5 | - thorough testing 6 | - utility: clonable and serializable (to/from json) 7 | - search values by prefix 8 | 9 | ## Install 10 | 11 | npm install --save digital-tree 12 | 13 | ***version 2.0.0 is almost a complete rewrite and mostly not backwards compatible*** 14 | 15 | ## API 16 | 17 | ### create() / Ctor 18 | 19 | using `create()`` is the recommended way to construct new digital trees: 20 | 21 | ```javascript 22 | const Trie = require('digital-tree') 23 | const trie = Trie.create() 24 | ``` 25 | 26 | ### put(key, value) 27 | 28 | Put something in the tree 29 | 30 | ```javascript 31 | trie.put(['a', 'path', 'to'], 'something') 32 | trie.put(['another', 'thing']) // equivalent to trie.put(['another', 'thing'], true) 33 | trie.put('strings also', 'work') // equivalent to trie.put('strings also'.split(''), 'work') 34 | 35 | // ** this only work with the exact key (reference) ** 36 | const objectKey = [{ foo: 'bar' }, { bar: 'foo'}] 37 | trie.put(objectKey, { some: 'thing'}) 38 | ``` 39 | 40 | ### get(key) 41 | 42 | Get something from the tree 43 | 44 | ```javascript 45 | const trie = Trie.create() 46 | trie.put(['a', 'path', 'to'], 'v1') 47 | trie.put('also strings', 'v2') 48 | 49 | console.log(trie.get([])) // prints 'foo' 50 | console.log(trie.get(Trie.root)) // prints 'foo' 51 | console.log(trie.get(['a', 'path', 'to'])) // prints 'v1' 52 | console.log(trie.get('also strings')) // prints 'v2' 53 | ``` 54 | 55 | ### Iteration 56 | 57 | A trie is iterable. Iteration order is either [DFS](https://en.wikipedia.org/wiki/Depth-first_search) or [BFS](https://en.wikipedia.org/wiki/Breadth-first_search) 58 | 59 | ```javascript 60 | const Trie = require('digital-tree') 61 | const trie = Trie.create() 62 | trie.put('abc', 1) 63 | trie.put('abd', 2) 64 | trie.put('abe', 3) 65 | 66 | for (let value of trie) { 67 | 68 | } 69 | ``` 70 | 71 | ### search(prefix) 72 | 73 | Search and return all the values in the tree that are nested under the provided `prefix`. 74 | 75 | The results will be an Iterator over the matching values. The order of iteration is defined based on the default ordering of the trie (BFS/DFS) 76 | 77 | ```javascript 78 | const trie = Trie.create() 79 | trie.put('abc', 1) 80 | trie.put('abd', 2) 81 | trie.put('abe', 3) 82 | 83 | console.log(Array.from(trie.search('ab'))) // prints [ 3, 2, 1 ] 84 | console.log(Array.from(trie.search('ab', { includeKeys: true }))) // prints [ [['a','b','e'], 3 ], [['a','b','d'], 2], [['a','b','c'], 1] ] 85 | ``` 86 | 87 | ### getSubTrie(key, [shallow=false]) 88 | 89 | Obtain either a cloned, or shallow copy of a subtree. 90 | 91 | ```javascript 92 | trie.put('abc', 1) 93 | trie.put('abd', 2) 94 | 95 | const subTrie = trie.getSubTrie('ab') 96 | 97 | console.log(subTrie.get('c')) // prints 1 98 | console.log(subTrie.get('d')) // prints 2 99 | console.log(subTrie.get('ab')) // prints undefined 100 | ``` 101 | *setting `shallow` to `true` will create a view rather than cloning the sub trie* 102 | 103 | ### remove(key) 104 | 105 | Remove something from the tree. This will remove the entire subtree that exists under this specified key and return it 106 | as a new trie. 107 | 108 | ```javascript 109 | trie.put(['a', 'b'], 'ab') 110 | trie.put(['a', 'b', 'c'], 'abc') 111 | trie.put(['a', 'b', 'c', 1], 'abc1') 112 | trie.put(['a', 'b', 'c', 2], 'abc2') 113 | 114 | const removed = trie.remove(['a', 'b', 'c']) 115 | 116 | console.log(removed.get([1])) // prints 'abc1' 117 | console.log(removed.get([2])) // prints 'abc2' 118 | 119 | console.log(trie.get(['a', 'b', 'c', 1])) // prints 'undefined' 120 | console.log(trie.get(['a', 'b'])) // prints 'ab' 121 | ``` 122 | -------------------------------------------------------------------------------- /test/HashMapNode.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const HashMapNode = require('../lib/HashMapNode') 3 | 4 | test('expose a value property', t => { 5 | const r = new HashMapNode('root') 6 | t.is(r.value, 'root') 7 | }) 8 | 9 | test('value property is mutable', t => { 10 | const r = new HashMapNode('root') 11 | r.value = 'foo' 12 | t.is(r.value, 'foo') 13 | }) 14 | 15 | test('children can be added with addChild(key, child) and accessed via getChild(key)', t => { 16 | const r = new HashMapNode('root') 17 | t.is(r.getChild('b'), undefined) 18 | const child = new HashMapNode('foo') 19 | r.addChild('b', child) 20 | t.is(r.getChild('b'), child) 21 | }) 22 | 23 | test('children can be removed using removeChild(key)', t => { 24 | const r = new HashMapNode('root') 25 | const child = new HashMapNode('foo') 26 | r.addChild('b', child) 27 | t.is(r.getChild('b'), child) 28 | r.removeChild('b') 29 | t.is(r.getChild('b'), undefined) 30 | }) 31 | 32 | test('iterable over its children', t => { 33 | const r = new HashMapNode('root') 34 | let children = Array.from(r) 35 | t.deepEqual(children, []) 36 | 37 | r.addChild('a', new HashMapNode('goo')) 38 | r.addChild('b', new HashMapNode('foo')) 39 | children = Array.from(r) 40 | t.deepEqual(children, [ 41 | [ 42 | 'a', r.getChild('a') 43 | ], 44 | [ 45 | 'b', r.getChild('b') 46 | ] 47 | ]) 48 | }) 49 | 50 | const expectedJSON = { 51 | value: 'root', 52 | children: { 53 | a: { 54 | value: 'goo', 55 | children: { 56 | c: { 57 | value: 'doo', 58 | children: {} 59 | } 60 | } 61 | }, 62 | b: { 63 | value: 'foo', 64 | children: {} 65 | } 66 | } 67 | } 68 | 69 | test('toJSON()', t => { 70 | const r = new HashMapNode('root') 71 | const c1 = new HashMapNode('goo') 72 | const c2 = new HashMapNode('foo') 73 | const c1c1 = new HashMapNode('doo') 74 | 75 | r.addChild('a', c1) 76 | r.addChild('b', c2) 77 | c1.addChild('c', c1c1) 78 | 79 | const serialized = r.toJSON() 80 | t.deepEqual(serialized, expectedJSON) 81 | }) 82 | 83 | test('fromJSON() - parameter is an object', t => { 84 | const node = HashMapNode.fromJSON(expectedJSON) 85 | t.is(node.value, 'root') 86 | t.is(node.getChild('a').value, 'goo') 87 | t.is(node.getChild('a').getChild('c').value, 'doo') 88 | t.is(node.getChild('b').value, 'foo') 89 | }) 90 | 91 | test('fromJSON() - parameter is a string', t => { 92 | const node = HashMapNode.fromJSON(JSON.stringify(expectedJSON)) 93 | t.is(node.value, 'root') 94 | t.is(node.getChild('a').value, 'goo') 95 | t.is(node.getChild('a').getChild('c').value, 'doo') 96 | t.is(node.getChild('b').value, 'foo') 97 | }) 98 | 99 | test('clone', t => { 100 | const r = new HashMapNode('root') 101 | const cra = new HashMapNode('cra') 102 | const crb = new HashMapNode('crb') 103 | 104 | r.addChild('a', cra) 105 | r.addChild('b', crb) 106 | 107 | const craa = new HashMapNode('craa') 108 | const crab = new HashMapNode('crab') 109 | 110 | const crba = new HashMapNode('crba') 111 | const crbb = new HashMapNode('crbb') 112 | 113 | cra.addChild('a', craa) 114 | cra.addChild('b', crab) 115 | 116 | crb.addChild('a', crba) 117 | crb.addChild('b', crbb) 118 | 119 | const crbaa = new HashMapNode('crbaa') 120 | const crbab = new HashMapNode('crbbb') 121 | 122 | crba.addChild('a', crbaa) 123 | crba.addChild('b', crbab) 124 | 125 | const clone = r.clone() 126 | t.is(clone.value, r.value) 127 | 128 | const [craClone, crbClone] = Array.from(clone.values()) 129 | t.is(craClone.value, cra.value) 130 | t.is(crbClone.value, crb.value) 131 | 132 | const [craaClone, crabClone] = Array.from(craClone.values()) 133 | t.is(craaClone.value, craa.value) 134 | t.is(crabClone.value, crab.value) 135 | 136 | const [crbaClone, crbbClone] = Array.from(crbClone.values()) 137 | t.is(crbaClone.value, crba.value) 138 | t.is(crbbClone.value, crbb.value) 139 | 140 | const [crbaaClone, crbabClone] = Array.from(crbaClone.values()) 141 | t.is(crbaaClone.value, crbaa.value) 142 | t.is(crbabClone.value, crbab.value) 143 | 144 | t.is(r.describe(), clone.describe()) 145 | }) -------------------------------------------------------------------------------- /lib/HashMapNode.js: -------------------------------------------------------------------------------- 1 | const debug = require('./debug')('HashMapNode') 2 | const NoopIterator = require('./NoopIterator') 3 | const noopIterator = new NoopIterator() 4 | const jsonStringify = require('json-stringify-safe') 5 | const { isString } = require('util') 6 | 7 | /** 8 | * internal class for nodes in the trie 9 | */ 10 | class HashMapNode { 11 | constructor(value) { 12 | if (debug.enabled) { 13 | HashMapNode.ctor++ 14 | } 15 | 16 | this._children = undefined 17 | this._value = value 18 | } 19 | 20 | get value() { 21 | return this._value 22 | } 23 | 24 | set value(value) { 25 | this._value = value 26 | } 27 | 28 | [Symbol.iterator]() { 29 | 30 | // it's quite annoying to have the if (this._children) {} guard 31 | // all around. I tested this on the benchmark lorem and found that 32 | // lazily creating the _children map will reduce approximately 20% 33 | // map instances so for now i decided it's worth it. 34 | // the number of unnecessary map instances will probably be bigger 35 | // in trees with lots of leaves in them. I think the penalty of several if 36 | // clauses should be insignificant 37 | if (this._children) { 38 | return this._children.entries() 39 | } 40 | 41 | return noopIterator 42 | } 43 | 44 | entries() { 45 | if (this._children) { 46 | return this._children.entries() 47 | } 48 | 49 | return noopIterator 50 | } 51 | 52 | values() { 53 | if (this._children) { 54 | return this._children.values() 55 | } 56 | 57 | return noopIterator 58 | } 59 | 60 | keys() { 61 | if (this._children) { 62 | return this._children.keys() 63 | } 64 | 65 | return noopIterator 66 | } 67 | 68 | getChild(key) { 69 | if (this._children) { 70 | return this._children.get(key) 71 | } 72 | } 73 | 74 | addChild(key, child) { 75 | 76 | // see the _children comment above first. 77 | // when debug is enabled I count these stats for all instances 78 | if (debug.enabled) { 79 | HashMapNode.add++ 80 | } 81 | 82 | debug('add child %s', key) 83 | 84 | if (!this._children) { 85 | if (debug.enabled) { 86 | HashMapNode.lazy++ 87 | } 88 | this._children = new Map() 89 | } 90 | 91 | this._children.set(key, child) 92 | } 93 | 94 | removeChild(key) { 95 | if (this._children) { 96 | this._children.delete(key) 97 | } 98 | } 99 | 100 | clone() { 101 | const clone = new HashMapNode(this._value) 102 | 103 | if (this._children) { 104 | for (let [key, child] of this) { 105 | clone.addChild(key, child.clone()) 106 | } 107 | } 108 | 109 | return clone 110 | } 111 | 112 | toJSON() { 113 | const result = { 114 | value: this.value, 115 | children: {} 116 | } 117 | 118 | for (let [key, child] of this.entries()) { 119 | result.children[key] = child.toJSON() 120 | } 121 | 122 | return result 123 | } 124 | 125 | static fromJSON(json) { 126 | if (isString(json)) { 127 | json = JSON.parse(json) 128 | } 129 | 130 | if (!json) { 131 | throw new Error('missing json value') 132 | } 133 | 134 | const root = new HashMapNode(json.value) 135 | 136 | for (let key in json.children) { 137 | const entry = json.children[key] 138 | if (!entry) { 139 | throw new Error('empty child entry in json') 140 | } 141 | 142 | const child = HashMapNode.fromJSON(entry) 143 | root.addChild(key, child) 144 | } 145 | 146 | return root 147 | } 148 | 149 | /** 150 | * describe this node in a pseudo json way 151 | * 152 | * @return {string} 153 | */ 154 | describe(_depth = 0) { 155 | let tabs = '' 156 | for (let i = 0; i < _depth; i++) { 157 | tabs += '\t' 158 | } 159 | 160 | let tabsPlus = `\n${tabs}\t` 161 | let result = `{${tabsPlus}value: ${this._value}` 162 | 163 | let children = Array.from(this) 164 | 165 | if (children.length > 0) { 166 | _depth++ 167 | result += `,${tabsPlus}children (${children.length}): {` 168 | for (let i = 0; i < children.length; i++) { 169 | let [key, child] = children[i] 170 | 171 | if (i > 0) { 172 | result += ',' 173 | } 174 | 175 | result += `${tabsPlus}\t${jsonStringify(key)}: ${child.describe(_depth + 1)}` 176 | } 177 | result += `${tabsPlus}}` 178 | } 179 | 180 | result += `\n${tabs}}` 181 | 182 | return result 183 | } 184 | } 185 | 186 | if (debug.enabled) { 187 | HashMapNode.ctor = 0 188 | HashMapNode.lazy = 0 189 | HashMapNode.add = 0 190 | } 191 | 192 | module.exports = HashMapNode -------------------------------------------------------------------------------- /lorem.js: -------------------------------------------------------------------------------- 1 | // used in benchmark tests 2 | module.exports = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec sem sapien. Maecenas non velit elementum, mollis turpis et, vehicula neque. In venenatis risus dolor, nec facilisis mauris viverra at. Nulla pellentesque bibendum sapien at faucibus. Praesent mattis congue nulla vel porta. In purus massa, iaculis eu augue vitae, lacinia semper purus. Suspendisse potenti. Aenean interdum facilisis odio, eget aliquam ligula auctor ac. Pellentesque sit amet nisl vitae enim iaculis suscipit nec in dolor. Fusce semper congue luctus. Phasellus iaculis, dui ut malesuada ultricies, ligula neque varius neque, eu ullamcorper erat lorem a magna. Cras feugiat, tortor at placerat elementum, leo purus vehicula diam, eget vehicula sapien diam ut sapien. Donec auctor semper eros, id rhoncus velit condimentum ut. Nam nec viverra quam, ac mattis erat. Curabitur sit amet enim sapien. Ut consequat id ante sit amet facilisis. Nam leo magna, pulvinar quis nisl eu, rhoncus vehicula est. Fusce adipiscing euismod dignissim. Aenean vel massa turpis. Mauris mattis nec urna sit amet porttitor. Aenean imperdiet, augue eu ullamcorper elementum, nulla urna feugiat nulla, in suscipit turpis purus nec nunc. Morbi aliquam sed eros id blandit. In a tincidunt leo, sed facilisis dui. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Etiam sit amet posuere urna. Maecenas placerat, lectus id adipiscing consequat, odio mauris accumsan libero, eget luctus erat ligula quis sapien. Pellentesque tellus augue, aliquam ac vulputate eu, lacinia eu sem. Morbi sagittis enim sed turpis convallis aliquet. Proin vehicula massa in sapien blandit, non ultrices leo convallis. In non orci porttitor, laoreet mauris id, euismod dolor. Mauris tristique odio eu augue sodales, quis ultrices arcu accumsan. Fusce quis justo eu quam rhoncus rhoncus id non nulla. Pellentesque blandit egestas enim, sed cursus purus porttitor eu. Nunc a mollis tortor. Morbi lobortis gravida tellus, id mattis purus placerat sit amet. Nullam rutrum dapibus orci. Vestibulum in suscipit massa, eget luctus lorem. Nulla tempus purus leo, et faucibus lectus congue nec. Quisque eu nulla fringilla, eleifend arcu eget, ornare leo. Vivamus consequat nunc nec tellus condimentum iaculis eu in odio. Curabitur nisi orci, tincidunt eget tellus volutpat, consectetur interdum est. Vestibulum porta ut ipsum sed adipiscing. Etiam lobortis fringilla vehicula. Nullam nec cursus tortor, sed hendrerit lectus. Pellentesque tincidunt accumsan lobortis. Integer congue orci a mattis luctus. Phasellus facilisis pulvinar urna. Nunc vel ipsum nibh. Ut adipiscing egestas nibh. Curabitur eget leo mattis, ullamcorper nulla ut, rhoncus tortor. Vivamus in erat et eros lacinia tempor. Pellentesque eget luctus ante. Proin quis lorem egestas, bibendum sem eu, rhoncus ante. Proin porttitor augue magna, dignissim sollicitudin neque vulputate nec. Maecenas ac pretium magna. Mauris tincidunt urna orci, non blandit felis convallis ac. Duis tempor euismod euismod. Integer sit amet lacus nibh. Vestibulum a lacinia felis. Curabitur tristique augue pellentesque, scelerisque erat vitae, commodo turpis. Proin et dapibus quam. Curabitur faucibus placerat sapien, quis malesuada lacus dignissim fringilla. Donec nec lectus et dui mollis aliquam. Phasellus molestie convallis dolor vitae eleifend. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec sem sapien. Maecenas non velit elementum, mollis turpis et, vehicula neque. In venenatis risus dolor, nec facilisis mauris viverra at. Nulla pellentesque bibendum sapien at faucibus. Praesent mattis congue nulla vel porta. In purus massa, iaculis eu augue vitae, lacinia semper purus. Suspendisse potenti. Aenean interdum facilisis odio, eget aliquam ligula auctor ac. Pellentesque sit amet nisl vitae enim iaculis suscipit nec in dolor. Fusce semper congue luctus. Phasellus iaculis, dui ut malesuada ultricies, ligula neque varius neque, eu ullamcorper erat lorem a magna. Cras feugiat, tortor at placerat elementum, leo purus vehicula diam, eget vehicula sapien diam ut sapien. Donec auctor semper eros, id rhoncus velit condimentum ut. Nam nec viverra quam, ac mattis erat. Curabitur sit amet enim sapien. Ut consequat id ante sit amet facilisis. Nam leo magna, pulvinar quis nisl eu, rhoncus vehicula est. Fusce adipiscing euismod dignissim. Aenean vel massa turpis. Mauris mattis nec urna sit amet porttitor. Aenean imperdiet, augue eu ullamcorper elementum, nulla urna feugiat nulla, in suscipit turpis purus nec nunc. Morbi aliquam sed eros id blandit. In a tincidunt leo, sed facilisis dui. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Etiam sit amet posuere urna. Maecenas placerat, lectus id adipiscing consequat, odio mauris accumsan libero, eget luctus erat ligula quis sapien. Pellentesque tellus augue, aliquam ac vulputate eu, lacinia eu sem. Morbi sagittis enim sed turpis convallis aliquet. Proin vehicula massa in sapien blandit, non ultrices leo convallis. In non orci porttitor, laoreet mauris id, euismod dolor. Mauris tristique odio eu augue sodales, quis ultrices arcu accumsan. Fusce quis justo eu quam rhoncus rhoncus id non nulla. Pellentesque blandit egestas enim, sed cursus purus porttitor eu. Nunc a mollis tortor. Morbi lobortis gravida tellus, id mattis purus placerat sit amet. Nullam rutrum dapibus orci. Vestibulum in suscipit massa, eget luctus lorem. Nulla tempus purus leo, et faucibus lectus congue nec. Quisque eu nulla fringilla, eleifend arcu eget, ornare leo. Vivamus consequat nunc nec tellus condimentum iaculis eu in odio. Curabitur nisi orci, tincidunt eget tellus volutpat, consectetur interdum est. Vestibulum porta ut ipsum sed adipiscing. Etiam lobortis fringilla vehicula. Nullam nec cursus tortor, sed hendrerit lectus. Pellentesque tincidunt accumsan lobortis. Integer congue orci a mattis luctus. Phasellus facilisis pulvinar urna. Nunc vel ipsum nibh. Ut adipiscing egestas nibh. Curabitur eget leo mattis, ullamcorper nulla ut, rhoncus tortor. Vivamus in erat et eros lacinia tempor. Pellentesque eget luctus ante. Proin quis lorem egestas, bibendum sem eu, rhoncus ante. Proin porttitor augue magna, dignissim sollicitudin neque vulputate nec. Maecenas ac pretium magna. Mauris tincidunt urna orci, non blandit felis convallis ac. Duis tempor euismod euismod. Integer sit amet lacus nibh. Vestibulum a lacinia felis. Curabitur tristique augue pellentesque, scelerisque erat vitae, commodo turpis. Proin et dapibus quam. Curabitur faucibus placerat sapien, quis malesuada lacus dignissim fringilla. Donec nec lectus et dui mollis aliquam. Phasellus molestie convallis dolor vitae eleifend.'.split(' ') -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { isString, isFunction } = require('util') 2 | const debug = require('./lib/debug')('Trie') 3 | const _DFS = Symbol('dfs') 4 | const _BFS = Symbol('bfs') 5 | const HashMapNode = require('./lib/HashMapNode') 6 | const TrieIterator = require('./lib/TrieIterator') 7 | const NoopIterator = require('./lib/NoopIterator') 8 | 9 | const noopIterator = new NoopIterator() 10 | 11 | class Trie { 12 | 13 | static get DFS() { return _DFS } 14 | static get BFS() { return _BFS } 15 | 16 | /** 17 | * Create a new Trie 18 | * 19 | * @param {variant} [options.rootValue] The root value of the trie, normally you will not use this. 20 | * @param {Symbol} [options.iterationOrder=Trie.DFS] The trie is `iterable`, setting this will change the default iteration order. 21 | * iteration order can be either `Trie.BFS` or `Trie.DFS`. You can still use an explicit iteration order by calling `trie.bfsIterator()` or `trie.dfsIterator()` 22 | * @param {HashMapNode} [options.NodeClass=HashMapNode] 23 | * 24 | * @return {Trie} 25 | */ 26 | static create({ 27 | iterationOrder = _DFS, 28 | NodeClass = HashMapNode 29 | } = {}) { 30 | 31 | if (iterationOrder !== _DFS && iterationOrder !== _BFS) { 32 | throw new Error('invalid iteration order, try Trie.BFS or Trie.DFS') 33 | } 34 | 35 | return new Trie({ NodeClass, iterationOrder }) 36 | } 37 | 38 | constructor({ NodeClass, iterationOrder }) { 39 | this._nodeClass = NodeClass 40 | this._root = this._newNode() 41 | this._iterationOrder = iterationOrder 42 | } 43 | 44 | /** 45 | * put something in the tree. 46 | * 47 | * @param {Iterable} key - each member in the array is a level in the tree 48 | * @param {variant} [value=true] - the value to store 49 | * 50 | */ 51 | put(key, value = true) { 52 | debug('put( {%s}, {%s} )', key, value) 53 | 54 | if (!this._isValidKey(key)) { 55 | throw new Error('invalid key') 56 | } 57 | 58 | let current = this._root 59 | let count = 0 60 | 61 | for (let part of key) { 62 | let node = current.getChild(part) 63 | 64 | if (node === undefined) { 65 | node = this._newNode() 66 | current.addChild(part, node) 67 | } 68 | 69 | count++ 70 | current = node 71 | } 72 | 73 | // prevent "losing" a value in root 74 | // if iterable was "empty" 75 | // empty interables doesn't make sense anyways 76 | if (count === 0) { 77 | throw new Error('invalid key') 78 | } 79 | 80 | current.value = value 81 | } 82 | 83 | putAll(iterable) { 84 | for (let key of iterable) { 85 | this.put(key, true) 86 | } 87 | } 88 | 89 | /** 90 | * get something from the tree. 91 | * 92 | * @param {Iterable} [key] - a path in the tree. 93 | * 94 | * @return {variant} the value that was placed under that key 95 | */ 96 | get(key) { 97 | debug('get( {%s} )', key) 98 | const current = this._getNode(key) 99 | if (current === undefined) return 100 | return current.value 101 | } 102 | 103 | /** 104 | * get a Trie view of the tree under "key" 105 | * 106 | * @param {Iterable} key 107 | * @param {boolean} [shallow] defaults to false 108 | * 109 | * @return {Trie} 110 | */ 111 | getSubTrie(key, shallow = false) { 112 | debug('getSubTrie( {%s} }', key) 113 | 114 | const current = this._getNode(key) 115 | return this._newTrieLikeThis(current, shallow) 116 | } 117 | 118 | /** 119 | * clone this trie. 120 | * 121 | * - Object keys will not be cloned 122 | * 123 | * @return {Trie} 124 | */ 125 | clone() { 126 | return this._newTrieLikeThis(this._root) 127 | } 128 | 129 | /** 130 | * remove something from the tree 131 | * 132 | * @param {Iterable} key 133 | * 134 | * @return {Trie} subtree that was removed 135 | */ 136 | remove(key) { 137 | // this whole thing can go away if HashMapNode will have a 138 | // parent reference... then removeChild() will not have 139 | // to accept keyPart 140 | const { current, parent, keyPart } = this._getNodeAndParent(key) 141 | 142 | if (current === undefined) return 143 | 144 | parent.removeChild(keyPart) 145 | 146 | return this._newTrieLikeThis(current) 147 | } 148 | 149 | /** 150 | * search for all values that are associated with keys that have the specified prefix 151 | * values will be ordered based on the default ordering of the trie (dfs/bfs) 152 | * 153 | * @param {Iterable} prefix 154 | * @param {boolean} [options.includeKeys=false] if set to true result will include keys as values. 155 | * @return {Iterable} 156 | */ 157 | search(prefix, { includeKeys = false } = {}) { 158 | if (!this._isValidKey(prefix)) { 159 | throw new Error('invalid key') 160 | } 161 | 162 | const node = this._getNode(prefix) 163 | 164 | if (node === undefined) { 165 | return noopIterator 166 | } 167 | 168 | return this._newTrieIterator(node, { includeKeys, iterationOrder: this._iterationOrder, prefix }) 169 | } 170 | 171 | [Symbol.iterator]() { 172 | return this._newTrieIterator(this._root) 173 | } 174 | 175 | /** 176 | * return a DFS iterator for this trie 177 | * 178 | * @param {boolean} [includeKeys=false] if set to true result will include keys as values. 179 | * @return {Iterator} 180 | */ 181 | dfsIterator(includeKeys = false) { 182 | return this._newTrieIterator(this._root, { includeKeys, iterationOrder: Trie.DFS }) 183 | } 184 | 185 | /** 186 | * return a BFS iterator for this trie 187 | * 188 | * @param {boolean} [includeKeys=false] if set to true result will include keys as values. 189 | * @return {Iterator} 190 | */ 191 | bfsIterator(includeKeys = false) { 192 | return this._newTrieIterator(this._root, { includeKeys, iterationOrder: Trie.BFS }) 193 | } 194 | 195 | _getNode(key) { 196 | if (!this._isValidKey(key)) { 197 | throw new Error('invalid key') 198 | } 199 | 200 | let current = this._root 201 | let count = 0 202 | 203 | for (let part of key) { 204 | let node = current.getChild(part) 205 | if (node === undefined) { 206 | return 207 | } 208 | 209 | count++ 210 | current = node 211 | } 212 | 213 | // prevent obtaining access to root 214 | // if iterable is empty 215 | if (count === 0) { 216 | throw new Error('invalid key') 217 | } 218 | 219 | return current 220 | } 221 | 222 | _getNodeAndParent(key) { 223 | 224 | if (!this._isValidKey(key)) { 225 | throw new Error('invalid key') 226 | } 227 | 228 | let keyPart = undefined 229 | let parent = undefined 230 | let current = this._root 231 | let count = 0 232 | for (keyPart of key) { 233 | let node = current.getChild(keyPart) 234 | if (node === undefined) { 235 | return {} 236 | } 237 | 238 | count++ 239 | parent = current 240 | current = node 241 | } 242 | 243 | // prevent getting access to root 244 | // if iterable is empty 245 | if (count > 0) { 246 | return { current, parent, keyPart } 247 | } 248 | 249 | throw new Error('invalid key') 250 | } 251 | 252 | _newNode(value) { 253 | return new this._nodeClass(value) 254 | } 255 | 256 | _isValidKey(key) { 257 | // const rightType = Array.isArray(key) || isString(key) 258 | // return rightType && key.length > 0 259 | return isFunction(key[Symbol.iterator]) 260 | } 261 | 262 | _newTrieLikeThis(root, shallow = false) { 263 | const trie = new Trie({ 264 | NodeClass: this._nodeClass, 265 | iterationOrder: this._iterationOrder 266 | }) 267 | 268 | trie._root = shallow ? root : root.clone() 269 | 270 | return trie 271 | } 272 | 273 | _newTrieIterator(rootNode, { includeKeys = false, iterationOrder = this._iterationOrder, prefix } = {}) { 274 | if (iterationOrder === _DFS) { 275 | return new TrieIterator(rootNode, { memory: TrieIterator.Stack, includeKeys, prefix }) 276 | } 277 | 278 | if (iterationOrder === _BFS) { 279 | return new TrieIterator(rootNode, { memory: TrieIterator.Queue, includeKeys, prefix }) 280 | } 281 | 282 | throw new Error('invalid iteration order, try Trie.BFS or Trie.DFS') 283 | } 284 | } 285 | 286 | module.exports = Trie -------------------------------------------------------------------------------- /test/Trie.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const Trie = require('../index') 3 | 4 | test('trie simple put() / get()', t => { 5 | const { subject } = t.context 6 | const key = [1, 2, 3] 7 | const value = 'foo' 8 | subject.put(key, value) 9 | t.is(subject.get(key), value) 10 | }) 11 | 12 | test('trie keys can be any iterable that has more than zero values', t => { 13 | const { subject } = t.context 14 | const iterable = new TestIterator(2) 15 | subject.put(iterable, 'foo') 16 | t.deepEqual(Array.from(subject), ['foo']) 17 | t.is(subject.get([1, 2]), 'foo') 18 | }) 19 | 20 | test('trie put throws an error if iterable has zero values', t => { 21 | const { subject } = t.context 22 | const iterable = new TestIterator(0) 23 | 24 | const error = t.throws(() => { 25 | subject.put(iterable, 'foo') 26 | }) 27 | 28 | t.is(error.message, 'invalid key') 29 | }) 30 | 31 | test('trie put() same key, different value', t => { 32 | const { subject } = t.context 33 | const key = [1, 2, 3] 34 | const v1 = 'foo' 35 | const v2 = 'foo' 36 | subject.put(key, v1) 37 | subject.put(key, v2) 38 | 39 | t.is(subject.get(key), v1) 40 | }) 41 | 42 | test('trie put() string)', t => { 43 | const { subject } = t.context 44 | const key = 'strings also work' 45 | const v1 = 'foo' 46 | subject.put(key, v1) 47 | 48 | t.is(subject.get(key), v1) // works with string 49 | t.is(subject.get(key.split('')), v1) // also an array with the same chars 50 | }) 51 | 52 | test('trie put() object)', t => { 53 | const { subject } = t.context 54 | 55 | const key = [{ foo: 'bar' }, { bar: 'foo' }] 56 | const v1 = 'foo' 57 | subject.put(key, v1) 58 | 59 | t.is(subject.get(key), v1) // works with string 60 | }) 61 | 62 | test('trie keys can be longer than one character', t => { 63 | const { subject } = t.context 64 | const key = ['foo', 'bar', 'meow'] 65 | subject.put(key, 1) 66 | 67 | t.is(subject.get(key), 1) 68 | }) 69 | 70 | test('trie put() invalid key', t => { 71 | const { subject } = t.context 72 | const error = t.throws(() => { 73 | subject.put(1, 1) 74 | }) 75 | t.is(error.message, 'invalid key') 76 | }) 77 | 78 | test('trie get() a non existent key', t => { 79 | const { subject } = t.context 80 | 81 | t.is(subject.get(['1']), undefined) 82 | }) 83 | 84 | test('trie get() empty key', t => { 85 | const { subject } = t.context 86 | 87 | const error = t.throws(() => { 88 | // empty iterable 89 | subject.get('') 90 | }) 91 | t.is(error.message, 'invalid key') 92 | }) 93 | 94 | test('trie get() invalid key', t => { 95 | const { subject } = t.context 96 | 97 | const error = t.throws(() => { 98 | // not iterable 99 | subject.get(1) 100 | }) 101 | t.is(error.message, 'invalid key') 102 | }) 103 | 104 | test('trie getSubTrie()', t => { 105 | const { subject } = t.context 106 | createMediumGraph(subject) 107 | const subTrie = subject.getSubTrie([1, 2, 3]) 108 | 109 | // [1,2,3,4] in original trie 110 | t.is(subTrie.get([4]), 'zoo') 111 | 112 | // [1,2,3,5] in original trie 113 | t.is(subTrie.get([5]), 'goo') 114 | }) 115 | 116 | test('trie getSubTrie() shallow === true', t => { 117 | const { subject } = t.context 118 | createMediumGraph(subject) 119 | const subTrie = subject.getSubTrie([1, 2, 3], true) 120 | 121 | // [1,2,3,4] in original trie 122 | t.is(subTrie.get([4]), 'zoo') 123 | 124 | // [1,2,3,5] in original trie 125 | t.is(subTrie.get([5]), 'goo') 126 | }) 127 | 128 | test('trie remove()', t => { 129 | const { subject } = t.context 130 | createMediumGraph(subject) 131 | const removed = subject.remove([1, 2, 3]) 132 | 133 | // [1,2,3,4] in original trie 134 | t.is(removed.get([4]), 'zoo') 135 | 136 | // [1,2,3,5] in original trie 137 | t.is(removed.get([5]), 'goo') 138 | 139 | t.is(subject.get([1, 2, 3]), undefined) 140 | }) 141 | 142 | test('trie remove() a non existent key', t => { 143 | const { subject } = t.context 144 | subject.put('abc', 'foo') 145 | const removed = subject.remove('bar') 146 | t.is(removed, undefined) 147 | t.is(subject.get('abc'), 'foo') 148 | }) 149 | 150 | test('trie remove() empty key', t => { 151 | const { subject } = t.context 152 | 153 | const error = t.throws(() => { 154 | subject.remove('') 155 | }) 156 | t.is(error.message, 'invalid key') 157 | }) 158 | 159 | test('trie remove() invalid key', t => { 160 | const { subject } = t.context 161 | 162 | const error = t.throws(() => { 163 | subject.remove(1) 164 | }) 165 | t.is(error.message, 'invalid key') 166 | }) 167 | 168 | test('iteration order is DFS by default', t => { 169 | const { subject } = t.context 170 | createBigGraph(subject) 171 | const iterationOrder = Array.from(subject) 172 | 173 | t.deepEqual(iterationOrder, EXPECTED_DFS_ITERATION_ORDER) 174 | }) 175 | 176 | test('Specify DFS iteration order in Ctor', t => { 177 | const subject = Trie.create({ iterationOrder: Trie.DFS }) 178 | createBigGraph(subject) 179 | const iterationOrder = Array.from(subject) 180 | 181 | t.deepEqual(iterationOrder, EXPECTED_DFS_ITERATION_ORDER) 182 | }) 183 | 184 | test('explicit DFS iterator', t => { 185 | const subject = Trie.create({ iterationOrder: Trie.BFS }) 186 | createBigGraph(subject) 187 | const iterationOrder = Array.from(subject.dfsIterator()) 188 | 189 | t.deepEqual(iterationOrder, EXPECTED_DFS_ITERATION_ORDER) 190 | }) 191 | 192 | test('Specify BFS iteration order in Ctor', t => { 193 | const subject = Trie.create({ iterationOrder: Trie.BFS }) 194 | createBigGraph(subject) 195 | const iterationOrder = Array.from(subject) 196 | 197 | t.deepEqual(iterationOrder, EXPECTED_BFS_ITERATION_ORDER) 198 | }) 199 | 200 | test('explicit BFS iterator', t => { 201 | const subject = Trie.create({ iterationOrder: Trie.DFS }) 202 | createBigGraph(subject) 203 | const iterationOrder = Array.from(subject.bfsIterator()) 204 | 205 | t.deepEqual(iterationOrder, EXPECTED_BFS_ITERATION_ORDER) 206 | }) 207 | 208 | test('trie clone', t => { 209 | const { subject } = t.context 210 | subject.put('ab', 'moo') 211 | subject.put('abc', 'foo') 212 | 213 | const clone = subject.clone() 214 | 215 | t.is(clone.get('ab'), 'moo') 216 | t.is(clone.get('abc'), 'foo') 217 | 218 | // make sure that stuff that's added to the 219 | // clone dont show up on the original 220 | clone.put('abcd', 'floop') 221 | t.is(subject.get('abcd'), undefined) 222 | 223 | // other way around 224 | subject.put('cbd', 'cbd') 225 | t.is(clone.get('cbd'), undefined) 226 | }) 227 | 228 | test('trie search(prefix)', t => { 229 | const { subject } = t.context 230 | createBigGraph(subject) 231 | const results = Array.from(subject.search([1, 2, 3])) 232 | t.deepEqual(results, ['foo', 'goo', 'zoo', 'fee']) 233 | }) 234 | 235 | test('trie search(prefix) include keys', t => { 236 | const { subject } = t.context 237 | createBigGraph(subject) 238 | const results = Array.from(subject.search([1, 2, 3], { includeKeys: true })) 239 | 240 | t.deepEqual(results, [ 241 | [ 242 | [1, 2, 3], 'foo' 243 | ], 244 | [ 245 | [1, 2, 3, 5], 'goo' 246 | ], 247 | [ 248 | [1, 2, 3, 4], 'zoo' 249 | ], 250 | [ 251 | [1, 2, 3, 4, 1], 'fee' 252 | ] 253 | ]) 254 | }) 255 | 256 | test('trie search(prefix) invalid key', t => { 257 | const { subject } = t.context 258 | 259 | const error = t.throws(() => { 260 | subject.search(1) 261 | }) 262 | t.is(error.message, 'invalid key') 263 | }) 264 | 265 | test('trie search(prefix) non existent prefix', t => { 266 | const { subject } = t.context 267 | const results = subject.search('kjhasd') 268 | t.deepEqual(Array.from(results), []) 269 | }) 270 | 271 | test('trie create() with invalid iteration ordering', t => { 272 | const error = t.throws(() => { 273 | Trie.create({ iterationOrder: 'bla' }) 274 | }) 275 | t.is(error.message, 'invalid iteration order, try Trie.BFS or Trie.DFS') 276 | }) 277 | 278 | test.beforeEach(t => { 279 | t.context.subject = Trie.create() 280 | }) 281 | 282 | const EXPECTED_BFS_ITERATION_ORDER = ['moo', 'foo', 'bar', 'zoo', 'goo', 'shmoo', 'fee'] 283 | const EXPECTED_DFS_ITERATION_ORDER = ['moo', 'bar', 'shmoo', 'foo', 'goo', 'zoo', 'fee'] 284 | 285 | function createBigGraph(subject) { 286 | subject.put([1, 2], 'moo') 287 | subject.put([1, 2, 3], 'foo') 288 | subject.put([1, 2, 4], 'bar') 289 | subject.put([1, 2, 4, 1], 'shmoo') 290 | subject.put([1, 2, 3, 4], 'zoo') 291 | subject.put([1, 2, 3, 5], 'goo') 292 | subject.put([1, 2, 3, 4, 1], 'fee') 293 | } 294 | 295 | function createMediumGraph(subject) { 296 | subject.put([1, 2], 'moo') 297 | subject.put([1, 2, 3], 'foo') 298 | subject.put([1, 2, 4], 'bar') 299 | subject.put([1, 2, 3, 4], 'zoo') 300 | subject.put([1, 2, 3, 5], 'goo') 301 | } 302 | 303 | class TestIterator { 304 | constructor(count) { 305 | this._count = count 306 | this._value = undefined 307 | this._done = false 308 | } 309 | 310 | next() { 311 | if (this._value === undefined) { 312 | this._value = 0 313 | } 314 | 315 | if (this._value === this._count) { 316 | this._done = true 317 | } 318 | 319 | this._value++ 320 | 321 | return this 322 | } 323 | 324 | get value() { 325 | return this._value 326 | } 327 | 328 | get done() { 329 | return this._done 330 | } 331 | 332 | [Symbol.iterator]() { 333 | return this 334 | } 335 | } --------------------------------------------------------------------------------