133 lines
4.5 KiB
JavaScript
133 lines
4.5 KiB
JavaScript
/**
|
|
* A string tree node consists of zero-or-more string keys, and a leaves property that contains any objects that
|
|
* terminate at the current node.
|
|
* @typedef {object} StringTreeNode
|
|
*/
|
|
|
|
/**
|
|
* @callback StringTreeEntryFilter
|
|
* @param {any} entry The entry to filter.
|
|
* @returns {boolean} Whether the entry should be included in the result set.
|
|
*/
|
|
|
|
/**
|
|
* A data structure representing a tree of string nodes with arbitrary object leaves.
|
|
*/
|
|
export default class StringTree {
|
|
/**
|
|
* The key symbol that stores the leaves of any given node.
|
|
* @type {symbol}
|
|
*/
|
|
static get leaves() {
|
|
return StringTree.#leaves;
|
|
}
|
|
|
|
static #leaves = Symbol();
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* The tree's root.
|
|
* @type {StringTreeNode}
|
|
*/
|
|
#root = this.#createNode();
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Create a new node.
|
|
* @returns {StringTreeNode}
|
|
*/
|
|
#createNode() {
|
|
return { [StringTree.leaves]: [] };
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Insert an entry into the tree.
|
|
* @param {string[]} strings The string parents for the entry.
|
|
* @param {any} entry The entry to store.
|
|
* @returns {StringTreeNode} The node the entry was added to.
|
|
*/
|
|
addLeaf(strings, entry) {
|
|
let node = this.#root;
|
|
for ( const string of strings ) {
|
|
node[string] ??= this.#createNode();
|
|
node = node[string];
|
|
}
|
|
|
|
// Once we've traversed the tree, we add our entry.
|
|
node[StringTree.leaves].push(entry);
|
|
return node;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Traverse the tree along the given string path and return any entries reachable from the node.
|
|
* @param {string[]} strings The string path to the desired node.
|
|
* @param {object} [options]
|
|
* @param {number} [options.limit] The maximum number of items to retrieve.
|
|
* @param {StringTreeEntryFilter} [options.filterEntries] A filter function to apply to each candidate entry.
|
|
* @returns {any[]}
|
|
*/
|
|
lookup(strings, { limit, filterEntries }={}) {
|
|
const entries = [];
|
|
const node = this.nodeAtPrefix(strings);
|
|
if ( !node ) return []; // No matching entries.
|
|
const queue = [node];
|
|
while ( queue.length ) {
|
|
if ( limit && (entries.length >= limit) ) break;
|
|
this._breadthFirstSearch(queue.shift(), entries, queue, { limit, filterEntries });
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Returns the node at the given path through the tree.
|
|
* @param {string[]} strings The string path to the desired node.
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.hasLeaves=false] Only return the most recently visited node that has leaves, otherwise
|
|
* return the exact node at the prefix, if it exists.
|
|
* @returns {StringTreeNode|void}
|
|
*/
|
|
nodeAtPrefix(strings, { hasLeaves=false }={}) {
|
|
let node = this.#root;
|
|
let withLeaves = node;
|
|
for ( const string of strings ) {
|
|
if ( !(string in node) ) return hasLeaves ? withLeaves : undefined;
|
|
node = node[string];
|
|
if ( node[StringTree.leaves].length ) withLeaves = node;
|
|
}
|
|
return hasLeaves ? withLeaves : node;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Perform a breadth-first search starting from the given node and retrieving any entries reachable from that node,
|
|
* until we reach the limit.
|
|
* @param {StringTreeNode} node The starting node.
|
|
* @param {any[]} entries The accumulated entries.
|
|
* @param {StringTreeNode[]} queue The working queue of nodes to search.
|
|
* @param {object} [options]
|
|
* @param {number} [options.limit] The maximum number of entries to retrieve before stopping.
|
|
* @param {StringTreeEntryFilter} [options.filterEntries] A filter function to apply to each candidate entry.
|
|
* @protected
|
|
*/
|
|
_breadthFirstSearch(node, entries, queue, { limit, filterEntries }={}) {
|
|
// Retrieve the entries at this node.
|
|
let leaves = node[StringTree.leaves];
|
|
if ( filterEntries instanceof Function ) leaves = leaves.filter(filterEntries);
|
|
entries.push(...leaves);
|
|
if ( limit && (entries.length >= limit) ) return;
|
|
// Push this node's children onto the end of the queue.
|
|
for ( const key of Object.keys(node) ) {
|
|
if ( typeof key === "string" ) queue.push(node[key]);
|
|
}
|
|
}
|
|
}
|