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