Initial
This commit is contained in:
132
resources/app/common/utils/string-tree.mjs
Normal file
132
resources/app/common/utils/string-tree.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user