@loken/hierarchies
Version:
Library for working with hierarchies of identifiers and identifiable objects.
403 lines (402 loc) • 15.3 kB
JavaScript
import { mapArgs, someToArray, mapGetLazy, someToIterable, MultiMap, Queue, isSomeItem } from "@loken/utilities";
import { traverseGraph } from "../traversal/graph-traverse.js";
import { ChildMap } from "../utilities/child-map.js";
import { HCNode } from "./node.js";
import { nodeToId, nodesToIds } from "./node-conversion.js";
import { flattenFullGraph, flattenGraph } from "../traversal/graph-flatten.js";
import { searchGraph, searchGraphMany } from "../traversal/graph-search.js";
import { flattenSequence } from "../traversal/sequence-flatten.js";
import { searchSequence, searchSequenceMany } from "../traversal/sequence-search.js";
class Nodes {
/**
* Create one or more nodes.
*
* @items One or more `Item`s to wrap in nodes.
* @returns One node when you pass one item and a fixed length tuple of nodes when you pass multiple items.
* @throws Must provide at least one argument.
*/
static create(...items) {
return mapArgs(items, (item) => new HCNode(item), !0, !1);
}
/**
* Create some nodes.
*
* @items One or more `Item`s to wrap in nodes.
* @returns An array of nodes containing the items.
* @throws Must provide at least one argument.
*/
static createSome(items) {
return someToArray(items).map((item) => new HCNode(item));
}
/**
* Build nodes of IDs linked as described by the provided `childMap`.
*
* @template Id The type of IDs.
* @param childMap The map describing the relations.
* @returns The root nodes.
*/
static assembleIds(childMap) {
const nodes = /* @__PURE__ */ new Map(), roots = [];
for (const parentId of childMap.keys()) {
const parentNode = new HCNode(parentId);
nodes.set(parentId, parentNode);
}
for (const [parentId, childIds] of childMap.entries()) {
const parentNode = nodes.get(parentId);
for (const childId of childIds) {
const childNode = mapGetLazy(nodes, childId, () => new HCNode(childId));
parentNode.attach(childNode);
}
}
for (const node of nodes.values())
node.isRoot && roots.push(node);
return roots;
}
/**
* Build nodes of IDs recursively from the property keys.
*
* @param source The object describing the relations.
* @param include Optional predicate used for determining whether a property should be included as an ID.
* @returns The root nodes.
*/
static assemblePropertyIds(source, include) {
return Nodes.assembleIds(ChildMap.fromPropertyIds(source, include));
}
/**
* Build nodes of `items` linked as described by the provided `childMap`.
*
* Will exclude any `items` that are not mentioned in the `childMap`.
*
* @template Item The type of item.
* @template Id The type of IDs.
* @param items The items to wrap in nodes.
* @param identify Means of getting an ID for an item.
* @param childMap The map describing the relations.
* @returns The root nodes.
*/
static assembleItems(items, identify, childMap) {
const roots = [], nodeMap = /* @__PURE__ */ new Map(), itemMap = /* @__PURE__ */ new Map(), getItem = (id) => {
if (!itemMap.has(id))
throw new Error(`Could not find item for mapped ID: ${id}`);
return itemMap.get(id);
};
for (const item of someToIterable(items))
itemMap.set(identify(item), item);
for (const [parentId, childIds] of childMap.entries()) {
const parentNode = mapGetLazy(nodeMap, parentId, () => new HCNode(getItem(parentId)));
for (const childId of childIds) {
const childNode = mapGetLazy(nodeMap, childId, () => new HCNode(getItem(childId)));
parentNode.attach(childNode);
}
}
for (const node of nodeMap.values())
node.isRoot && roots.push(node);
return roots;
}
/**
* Build nodes of `Item`s linked as described by the provided `roots` and `children` delegate.
*
* @template Item The type of item.
* @template Id The type of IDs.
* @param roots The root items to wrap in nodes.
* @param children The delegate for getting the child items from a parent item.
* @returns The root nodes.
*/
static assembleItemsWithChildren(roots, children) {
const rootNodes = Nodes.createSome(roots);
return flattenFullGraph({
roots: rootNodes,
next: (node) => {
const childItems = children(node.item);
if (childItems != null && childItems.length) {
const childNodes = childItems.map((childItem) => new HCNode(childItem));
return node.attach(childNodes), childNodes;
}
}
}), rootNodes;
}
/**
* Build nodes of ´Item´s linked as described by the provided `leaves` and `parent` delegate.
*
* @template Item The type of item.
* @template Id The type of IDs.
* @param leaves The leaf items to wrap in nodes.
* @param parent The delegate for getting the parent of an item.
* @returns The root nodes.
*/
static assembleItemsWithParents(leaves, parent) {
const nodes = /* @__PURE__ */ new Map(), roots = [];
for (const leaf of someToIterable(leaves)) {
let current = {
item: leaf,
node: getNode(leaf)
};
for (; current; ) {
const parentItem = parent(current.item);
if (parentItem !== void 0) {
const parentSeen = nodes.has(parentItem), parentNode = getNode(parentItem);
if (parentNode.attach(current.node), parentSeen)
break;
current = {
item: parentItem,
node: parentNode
};
} else
roots.push(current.node), current = void 0;
}
}
return roots;
function getNode(item) {
let node = nodes.get(item);
return node || (node = new HCNode(item), nodes.set(item, node)), node;
}
}
/**
* Create a map of ids to child-ids by traversing the graph of the `roots`.
*
* @template Item The type of item.
* @template Id The type of IDs.
* @param roots The roots to use for traversal.
* @param identify Means of getting an ID for an item.
* @param childMap An existing or new child-map.
* @returns A parent-to-child map of IDs.
*/
static toChildMap(roots, identify, childMap = new MultiMap()) {
for (const root of someToIterable(roots)) {
const nodeId = nodeToId(root, identify);
if (root.isInternal) {
const childIds = nodesToIds(root.getChildren(), identify);
childMap.add(nodeId, childIds);
} else
childMap.getOrAdd(nodeToId(root, identify));
}
const nodes = flattenFullGraph({
roots,
next: (node) => node.getChildren().filter((n) => n.isInternal)
});
for (const node of nodes) {
const childNodes = node.getChildren(), nodeId = nodeToId(node, identify), childIds = nodesToIds(childNodes, identify);
childMap.add(nodeId, childIds);
}
return childMap;
}
/**
* Create a map of ids to descendant-ids by traversing the graph of the `roots`.
*
* @template Item The type of item.
* @template Id The type of IDs.
* @param roots The roots to use for traversal.
* @param identify Means of getting an ID for an item.
* @param descendantMap An existing or new descendant-map.
* @returns A parent-to-descendant map of IDs.
*/
static toDescendantMap(roots, identify, descendantMap = new MultiMap()) {
roots = someToArray(roots);
const store = new Queue();
store.enqueue(roots.map((node) => [node, []]));
for (const root of roots)
descendantMap.getOrAdd(nodeToId(root, identify));
for (; store.count > 0; ) {
const [node, ancestors] = store.dequeue(), nodeId = nodeToId(node, identify);
for (const ancestor of ancestors)
ancestor.add(nodeId);
const children = node.getChildren();
if (children != null && children.length) {
const nodeDescendants = descendantMap.getOrAdd(nodeId), childAncestors = [...ancestors, nodeDescendants];
store.enqueue(children.map((node2) => [node2, childAncestors]));
}
}
return descendantMap;
}
/**
* Create a map of ids to ancestor-ids by traversing the graph of the `roots`.
*
* @template Item The type of item.
* @template Id The type of IDs.
* @param roots The roots to use for traversal.
* @param identify Means of getting an ID for an item.
* @param ancestorMap An existing or new ancestor-map.
* @returns A child-to-ancestor map of IDs.
*/
static toAncestorMap(roots, identify, ancestorMap = new MultiMap()) {
roots = someToArray(roots);
const store = new Queue();
store.enqueue(roots.map((node) => [node]));
for (const root of roots)
root.isLeaf && ancestorMap.getOrAdd(nodeToId(root, identify));
for (; store.count > 0; ) {
const [node, ancestors] = store.dequeue(), nodeId = nodeToId(node, identify);
ancestors && ancestorMap.add(nodeId, ancestors);
const children = node.getChildren();
if (children != null && children.length) {
const childAncestors = ancestors ? [nodeId, ...ancestors] : [nodeId];
store.enqueue(children.map((node2) => [node2, childAncestors]));
}
}
return ancestorMap;
}
/**
* Create a list of relations by traversing the graph of the `roots`.
*
* @template Item The type of item.
* @template Id The type of IDs.
* @param roots The roots to use for traversal.
* @param identify Means of getting an ID for an item.
* @returns An array of `Relation<Id>`s.
*/
static toRelations(roots, identify) {
const relations = [];
return flattenFullGraph({
roots,
next: (node) => {
if (node.isLeaf)
return;
const children = node.getChildren(), childIds = nodesToIds(children, identify), nodeId = nodeToId(node, identify);
for (const childId of childIds)
relations.push([nodeId, childId]);
return children.filter((child) => child.isInternal);
}
}), relations;
}
/** Get unique ancestor nodes. */
static getAncestors(nodes, includeSelf = !1) {
if (isSomeItem(nodes))
return nodes.getAncestors(includeSelf);
const seen = /* @__PURE__ */ new Set(), ancestors = [];
for (const node of someToIterable(nodes)) {
const first = includeSelf ? node : node.getParent();
if (!first || seen.has(first))
continue;
const unseenAncestors = flattenSequence({
first,
next: (node2) => {
const parent = node2.getParent();
if (!(!parent || seen.has(parent)))
return seen.add(node2), parent;
}
});
ancestors.push(...unseenAncestors);
}
return ancestors;
}
/** Get ancestor items from unique ancestor nodes. */
static getAncestorItems(nodes, includeSelf = !1) {
return this.getAncestors(nodes, includeSelf).map((node) => node.item);
}
/** Get descendant nodes by traversing according to the options. */
static getDescendants(roots, includeSelf = !1, type = "breadth-first") {
return flattenGraph({
roots: HCNode.getRoots(roots, includeSelf),
next: (node) => node.getChildren(),
type
});
}
/** Get descendant items by traversing according to the options. */
static getDescendantItems(roots, includeSelf = !1, type = "breadth-first") {
return this.getDescendants(roots, includeSelf, type).map((node) => node.item);
}
/** Generate a sequence of descendant nodes by traversing according to the options. */
static traverseDescendants(roots, includeSelf = !1, type = "breadth-first") {
return traverseGraph({
roots: HCNode.getRoots(roots, includeSelf),
next: (node) => node.getChildren(),
type
});
}
/** Find the common ancestor node which is the closest to the `nodes`. */
static findCommonAncestor(nodes, includeSelf = !1) {
const commonAncestors = this.findCommonAncestorSet(nodes, includeSelf);
return commonAncestors == null ? void 0 : commonAncestors.values().next().value;
}
/** Find the ancestor nodes common to the `nodes`. */
static findCommonAncestors(nodes, includeSelf = !1) {
const commonAncestors = this.findCommonAncestorSet(nodes, includeSelf);
return commonAncestors ? [...commonAncestors] : void 0;
}
/** Find the ancestor nodes common to the `nodes`. */
static findCommonAncestorItems(nodes, includeSelf = !1) {
const commonAncestors = this.findCommonAncestorSet(nodes, includeSelf);
return commonAncestors ? [...commonAncestors].map((n) => n.item) : void 0;
}
/** Find the set of ancestor nodes common to the `nodes`. */
static findCommonAncestorSet(nodes, includeSelf = !1) {
return someToArray(nodes).reduce((acc, curr) => {
const ancestors = new Set(curr.getAncestors(includeSelf));
return acc ? acc.intersection(ancestors) : ancestors;
}, null);
}
/** Find the first ancestor node matching the `search`. */
static findAncestor(roots, search, includeSelf = !1) {
if (isSomeItem(roots))
return roots.findAncestor(search, includeSelf);
const seen = /* @__PURE__ */ new Set();
for (const root of roots) {
const first = includeSelf ? root : root.getParent();
if (!first || seen.has(first))
continue;
const found = searchSequence({
first,
next: (node) => {
const parent = node.getParent();
if (!(!parent || seen.has(parent)))
return seen.add(node), parent;
},
search
});
if (found)
return found;
}
}
/** Find the first ancestor node matching the `search`. */
static findAncestors(roots, search, includeSelf = !1) {
if (isSomeItem(roots))
return roots.findAncestors(search, includeSelf);
const seen = /* @__PURE__ */ new Set(), ancestors = [];
for (const root of roots) {
const first = includeSelf ? root : root.getParent();
if (!first || seen.has(first))
continue;
const found = searchSequenceMany({
first,
next: (node) => {
const parent = node.getParent();
if (!(!parent || seen.has(parent)))
return seen.add(node), parent;
},
search
});
ancestors.push(...found);
}
return ancestors;
}
/** Find the first descendant node matching the `search`. */
static findDescendant(roots, search, includeSelf = !1, type = "breadth-first") {
return searchGraph({
roots: HCNode.getRoots(roots, includeSelf),
next: (node) => node.getChildren(),
search,
type
});
}
/** Find the descendant nodes matching the `search`. */
static findDescendants(roots, search, includeSelf = !1, type = "breadth-first") {
return searchGraphMany({
roots: HCNode.getRoots(roots, includeSelf),
next: (node) => node.getChildren(),
search,
type
});
}
/** Does an ancestor node matching the `search` exist? */
static hasAncestor(roots, search, includeSelf = !1) {
return this.findAncestor(roots, search, includeSelf) !== void 0;
}
/** Does a descendant node matching the `search` exist? */
static hasDescendant(roots, search, includeSelf = !1, type = "breadth-first") {
return this.findDescendant(roots, search, includeSelf, type) !== void 0;
}
}
export {
Nodes
};
//# sourceMappingURL=nodes.js.map