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;
|