UNPKG

@loken/hierarchies

Version:

Library for working with hierarchies of identifiers and identifiable objects.

403 lines (402 loc) 15.3 kB
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