You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					478 lines
				
				9.8 KiB
			
		
		
			
		
	
	
					478 lines
				
				9.8 KiB
			| 
											3 years ago
										 | /** | ||
|  |  * Mnemonist TrieMap | ||
|  |  * ================== | ||
|  |  * | ||
|  |  * JavaScript TrieMap implementation based upon plain objects. As such this | ||
|  |  * structure is more a convenience building upon the trie's advantages than | ||
|  |  * a real performant alternative to already existing structures. | ||
|  |  * | ||
|  |  * Note that the Trie is based upon the TrieMap since the underlying machine | ||
|  |  * is the very same. The Trie just does not let you set values and only | ||
|  |  * considers the existence of the given prefixes. | ||
|  |  */ | ||
|  | var forEach = require('obliterator/foreach'), | ||
|  |     Iterator = require('obliterator/iterator'); | ||
|  | 
 | ||
|  | /** | ||
|  |  * Constants. | ||
|  |  */ | ||
|  | var SENTINEL = String.fromCharCode(0); | ||
|  | 
 | ||
|  | /** | ||
|  |  * TrieMap. | ||
|  |  * | ||
|  |  * @constructor | ||
|  |  */ | ||
|  | function TrieMap(Token) { | ||
|  |   this.mode = Token === Array ? 'array' : 'string'; | ||
|  |   this.clear(); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to clear the trie. | ||
|  |  * | ||
|  |  * @return {undefined} | ||
|  |  */ | ||
|  | TrieMap.prototype.clear = function() { | ||
|  | 
 | ||
|  |   // Properties
 | ||
|  |   this.root = {}; | ||
|  |   this.size = 0; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to set the value of the given prefix in the trie. | ||
|  |  * | ||
|  |  * @param  {string|array} prefix - Prefix to follow. | ||
|  |  * @param  {any}          value  - Value for the prefix. | ||
|  |  * @return {TrieMap} | ||
|  |  */ | ||
|  | TrieMap.prototype.set = function(prefix, value) { | ||
|  |   var node = this.root, | ||
|  |       token; | ||
|  | 
 | ||
|  |   for (var i = 0, l = prefix.length; i < l; i++) { | ||
|  |     token = prefix[i]; | ||
|  | 
 | ||
|  |     node = node[token] || (node[token] = {}); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Do we need to increase size?
 | ||
|  |   if (!(SENTINEL in node)) | ||
|  |     this.size++; | ||
|  | 
 | ||
|  |   node[SENTINEL] = value; | ||
|  | 
 | ||
|  |   return this; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to update the value of the given prefix in the trie. | ||
|  |  * | ||
|  |  * @param  {string|array} prefix - Prefix to follow. | ||
|  |  * @param  {(oldValue: any | undefined) => any} updateFunction - Update value visitor callback. | ||
|  |  * @return {TrieMap} | ||
|  |  */ | ||
|  | TrieMap.prototype.update = function(prefix, updateFunction) { | ||
|  |   var node = this.root, | ||
|  |       token; | ||
|  | 
 | ||
|  |   for (var i = 0, l = prefix.length; i < l; i++) { | ||
|  |     token = prefix[i]; | ||
|  | 
 | ||
|  |     node = node[token] || (node[token] = {}); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Do we need to increase size?
 | ||
|  |   if (!(SENTINEL in node)) | ||
|  |     this.size++; | ||
|  | 
 | ||
|  |   node[SENTINEL] = updateFunction(node[SENTINEL]); | ||
|  | 
 | ||
|  |   return this; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to return the value sitting at the end of the given prefix or | ||
|  |  * undefined if none exist. | ||
|  |  * | ||
|  |  * @param  {string|array} prefix - Prefix to follow. | ||
|  |  * @return {any|undefined} | ||
|  |  */ | ||
|  | TrieMap.prototype.get = function(prefix) { | ||
|  |   var node = this.root, | ||
|  |       token, | ||
|  |       i, | ||
|  |       l; | ||
|  | 
 | ||
|  |   for (i = 0, l = prefix.length; i < l; i++) { | ||
|  |     token = prefix[i]; | ||
|  |     node = node[token]; | ||
|  | 
 | ||
|  |     // Prefix does not exist
 | ||
|  |     if (typeof node === 'undefined') | ||
|  |       return; | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!(SENTINEL in node)) | ||
|  |     return; | ||
|  | 
 | ||
|  |   return node[SENTINEL]; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to delete a prefix from the trie. | ||
|  |  * | ||
|  |  * @param  {string|array} prefix - Prefix to delete. | ||
|  |  * @return {boolean} | ||
|  |  */ | ||
|  | TrieMap.prototype.delete = function(prefix) { | ||
|  |   var node = this.root, | ||
|  |       toPrune = null, | ||
|  |       tokenToPrune = null, | ||
|  |       parent, | ||
|  |       token, | ||
|  |       i, | ||
|  |       l; | ||
|  | 
 | ||
|  |   for (i = 0, l = prefix.length; i < l; i++) { | ||
|  |     token = prefix[i]; | ||
|  |     parent = node; | ||
|  |     node = node[token]; | ||
|  | 
 | ||
|  |     // Prefix does not exist
 | ||
|  |     if (typeof node === 'undefined') | ||
|  |       return false; | ||
|  | 
 | ||
|  |     // Keeping track of a potential branch to prune
 | ||
|  |     if (toPrune !== null) { | ||
|  |       if (Object.keys(node).length > 1) { | ||
|  |         toPrune = null; | ||
|  |         tokenToPrune = null; | ||
|  |       } | ||
|  |     } | ||
|  |     else { | ||
|  |       if (Object.keys(node).length < 2) { | ||
|  |         toPrune = parent; | ||
|  |         tokenToPrune = token; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!(SENTINEL in node)) | ||
|  |     return false; | ||
|  | 
 | ||
|  |   this.size--; | ||
|  | 
 | ||
|  |   if (toPrune) | ||
|  |     delete toPrune[tokenToPrune]; | ||
|  |   else | ||
|  |     delete node[SENTINEL]; | ||
|  | 
 | ||
|  |   return true; | ||
|  | }; | ||
|  | 
 | ||
|  | // TODO: add #.prune?
 | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to assert whether the given prefix exists in the TrieMap. | ||
|  |  * | ||
|  |  * @param  {string|array} prefix - Prefix to check. | ||
|  |  * @return {boolean} | ||
|  |  */ | ||
|  | TrieMap.prototype.has = function(prefix) { | ||
|  |   var node = this.root, | ||
|  |       token; | ||
|  | 
 | ||
|  |   for (var i = 0, l = prefix.length; i < l; i++) { | ||
|  |     token = prefix[i]; | ||
|  |     node = node[token]; | ||
|  | 
 | ||
|  |     if (typeof node === 'undefined') | ||
|  |       return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   return SENTINEL in node; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method used to retrieve every item in the trie with the given prefix. | ||
|  |  * | ||
|  |  * @param  {string|array} prefix - Prefix to query. | ||
|  |  * @return {array} | ||
|  |  */ | ||
|  | TrieMap.prototype.find = function(prefix) { | ||
|  |   var isString = typeof prefix === 'string'; | ||
|  | 
 | ||
|  |   var node = this.root, | ||
|  |       matches = [], | ||
|  |       token, | ||
|  |       i, | ||
|  |       l; | ||
|  | 
 | ||
|  |   for (i = 0, l = prefix.length; i < l; i++) { | ||
|  |     token = prefix[i]; | ||
|  |     node = node[token]; | ||
|  | 
 | ||
|  |     if (typeof node === 'undefined') | ||
|  |       return matches; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Performing DFS from prefix
 | ||
|  |   var nodeStack = [node], | ||
|  |       prefixStack = [prefix], | ||
|  |       k; | ||
|  | 
 | ||
|  |   while (nodeStack.length) { | ||
|  |     prefix = prefixStack.pop(); | ||
|  |     node = nodeStack.pop(); | ||
|  | 
 | ||
|  |     for (k in node) { | ||
|  |       if (k === SENTINEL) { | ||
|  |         matches.push([prefix, node[SENTINEL]]); | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       nodeStack.push(node[k]); | ||
|  |       prefixStack.push(isString ? prefix + k : prefix.concat(k)); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return matches; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method returning an iterator over the trie's values. | ||
|  |  * | ||
|  |  * @param  {string|array} [prefix] - Optional starting prefix. | ||
|  |  * @return {Iterator} | ||
|  |  */ | ||
|  | TrieMap.prototype.values = function(prefix) { | ||
|  |   var node = this.root, | ||
|  |       nodeStack = [], | ||
|  |       token, | ||
|  |       i, | ||
|  |       l; | ||
|  | 
 | ||
|  |   // Resolving initial prefix
 | ||
|  |   if (prefix) { | ||
|  |     for (i = 0, l = prefix.length; i < l; i++) { | ||
|  |       token = prefix[i]; | ||
|  |       node = node[token]; | ||
|  | 
 | ||
|  |       // If the prefix does not exist, we return an empty iterator
 | ||
|  |       if (typeof node === 'undefined') | ||
|  |         return Iterator.empty(); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   nodeStack.push(node); | ||
|  | 
 | ||
|  |   return new Iterator(function() { | ||
|  |     var currentNode, | ||
|  |         hasValue = false, | ||
|  |         k; | ||
|  | 
 | ||
|  |     while (nodeStack.length) { | ||
|  |       currentNode = nodeStack.pop(); | ||
|  | 
 | ||
|  |       for (k in currentNode) { | ||
|  |         if (k === SENTINEL) { | ||
|  |           hasValue = true; | ||
|  |           continue; | ||
|  |         } | ||
|  | 
 | ||
|  |         nodeStack.push(currentNode[k]); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (hasValue) | ||
|  |         return {done: false, value: currentNode[SENTINEL]}; | ||
|  |     } | ||
|  | 
 | ||
|  |     return {done: true}; | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method returning an iterator over the trie's prefixes. | ||
|  |  * | ||
|  |  * @param  {string|array} [prefix] - Optional starting prefix. | ||
|  |  * @return {Iterator} | ||
|  |  */ | ||
|  | TrieMap.prototype.prefixes = function(prefix) { | ||
|  |   var node = this.root, | ||
|  |       nodeStack = [], | ||
|  |       prefixStack = [], | ||
|  |       token, | ||
|  |       i, | ||
|  |       l; | ||
|  | 
 | ||
|  |   var isString = this.mode === 'string'; | ||
|  | 
 | ||
|  |   // Resolving initial prefix
 | ||
|  |   if (prefix) { | ||
|  |     for (i = 0, l = prefix.length; i < l; i++) { | ||
|  |       token = prefix[i]; | ||
|  |       node = node[token]; | ||
|  | 
 | ||
|  |       // If the prefix does not exist, we return an empty iterator
 | ||
|  |       if (typeof node === 'undefined') | ||
|  |         return Iterator.empty(); | ||
|  |     } | ||
|  |   } | ||
|  |   else { | ||
|  |     prefix = isString ? '' : []; | ||
|  |   } | ||
|  | 
 | ||
|  |   nodeStack.push(node); | ||
|  |   prefixStack.push(prefix); | ||
|  | 
 | ||
|  |   return new Iterator(function() { | ||
|  |     var currentNode, | ||
|  |         currentPrefix, | ||
|  |         hasValue = false, | ||
|  |         k; | ||
|  | 
 | ||
|  |     while (nodeStack.length) { | ||
|  |       currentNode = nodeStack.pop(); | ||
|  |       currentPrefix = prefixStack.pop(); | ||
|  | 
 | ||
|  |       for (k in currentNode) { | ||
|  |         if (k === SENTINEL) { | ||
|  |           hasValue = true; | ||
|  |           continue; | ||
|  |         } | ||
|  | 
 | ||
|  |         nodeStack.push(currentNode[k]); | ||
|  |         prefixStack.push(isString ? currentPrefix + k : currentPrefix.concat(k)); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (hasValue) | ||
|  |         return {done: false, value: currentPrefix}; | ||
|  |     } | ||
|  | 
 | ||
|  |     return {done: true}; | ||
|  |   }); | ||
|  | }; | ||
|  | TrieMap.prototype.keys = TrieMap.prototype.prefixes; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Method returning an iterator over the trie's entries. | ||
|  |  * | ||
|  |  * @param  {string|array} [prefix] - Optional starting prefix. | ||
|  |  * @return {Iterator} | ||
|  |  */ | ||
|  | TrieMap.prototype.entries = function(prefix) { | ||
|  |   var node = this.root, | ||
|  |       nodeStack = [], | ||
|  |       prefixStack = [], | ||
|  |       token, | ||
|  |       i, | ||
|  |       l; | ||
|  | 
 | ||
|  |   var isString = this.mode === 'string'; | ||
|  | 
 | ||
|  |   // Resolving initial prefix
 | ||
|  |   if (prefix) { | ||
|  |     for (i = 0, l = prefix.length; i < l; i++) { | ||
|  |       token = prefix[i]; | ||
|  |       node = node[token]; | ||
|  | 
 | ||
|  |       // If the prefix does not exist, we return an empty iterator
 | ||
|  |       if (typeof node === 'undefined') | ||
|  |         return Iterator.empty(); | ||
|  |     } | ||
|  |   } | ||
|  |   else { | ||
|  |     prefix = isString ? '' : []; | ||
|  |   } | ||
|  | 
 | ||
|  |   nodeStack.push(node); | ||
|  |   prefixStack.push(prefix); | ||
|  | 
 | ||
|  |   return new Iterator(function() { | ||
|  |     var currentNode, | ||
|  |         currentPrefix, | ||
|  |         hasValue = false, | ||
|  |         k; | ||
|  | 
 | ||
|  |     while (nodeStack.length) { | ||
|  |       currentNode = nodeStack.pop(); | ||
|  |       currentPrefix = prefixStack.pop(); | ||
|  | 
 | ||
|  |       for (k in currentNode) { | ||
|  |         if (k === SENTINEL) { | ||
|  |           hasValue = true; | ||
|  |           continue; | ||
|  |         } | ||
|  | 
 | ||
|  |         nodeStack.push(currentNode[k]); | ||
|  |         prefixStack.push(isString ? currentPrefix + k : currentPrefix.concat(k)); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (hasValue) | ||
|  |         return {done: false, value: [currentPrefix, currentNode[SENTINEL]]}; | ||
|  |     } | ||
|  | 
 | ||
|  |     return {done: true}; | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Attaching the #.entries method to Symbol.iterator if possible. | ||
|  |  */ | ||
|  | if (typeof Symbol !== 'undefined') | ||
|  |   TrieMap.prototype[Symbol.iterator] = TrieMap.prototype.entries; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Convenience known methods. | ||
|  |  */ | ||
|  | TrieMap.prototype.inspect = function() { | ||
|  |   var proxy = new Array(this.size); | ||
|  | 
 | ||
|  |   var iterator = this.entries(), | ||
|  |       step, | ||
|  |       i = 0; | ||
|  | 
 | ||
|  |   while ((step = iterator.next(), !step.done)) | ||
|  |     proxy[i++] = step.value; | ||
|  | 
 | ||
|  |   // Trick so that node displays the name of the constructor
 | ||
|  |   Object.defineProperty(proxy, 'constructor', { | ||
|  |     value: TrieMap, | ||
|  |     enumerable: false | ||
|  |   }); | ||
|  | 
 | ||
|  |   return proxy; | ||
|  | }; | ||
|  | 
 | ||
|  | if (typeof Symbol !== 'undefined') | ||
|  |   TrieMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = TrieMap.prototype.inspect; | ||
|  | 
 | ||
|  | TrieMap.prototype.toJSON = function() { | ||
|  |   return this.root; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Static @.from function taking an arbitrary iterable & converting it into | ||
|  |  * a trie. | ||
|  |  * | ||
|  |  * @param  {Iterable} iterable   - Target iterable. | ||
|  |  * @return {TrieMap} | ||
|  |  */ | ||
|  | TrieMap.from = function(iterable) { | ||
|  |   var trie = new TrieMap(); | ||
|  | 
 | ||
|  |   forEach(iterable, function(value, key) { | ||
|  |     trie.set(key, value); | ||
|  |   }); | ||
|  | 
 | ||
|  |   return trie; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Exporting. | ||
|  |  */ | ||
|  | TrieMap.SENTINEL = SENTINEL; | ||
|  | module.exports = TrieMap; |