UNPKG

@loken/hierarchies

Version:

Library for working with hierarchies of identifiers and identifiable objects.

595 lines (491 loc) 17 kB
import { isSomeItem, mapArgs, mapGetLazy, MultiMap, Queue, type Some, someToArray, someToIterable } from '@loken/utilities'; import { traverseGraph } from '../traversal/graph-traverse.js'; import type { TraversalType } from '../traversal/graph.types.js'; import { ChildMap } from '../utilities/child-map.js'; import type { Identify } from '../utilities/identify.js'; import type { GetChildren, GetParent } from '../utilities/related-items.js'; import type { Relation } from '../utilities/relations.js'; import { HCNode } from './node.js'; import type { NodePredicate } from './node.types.js'; import { nodesToIds, nodeToId } 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'; export 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. */ public static create<Items extends any[]>(...items: Items) { return mapArgs(items, item => new HCNode(item), true, false); } /** * 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. */ public static createSome<Item>(items: Some<Item>) { 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. */ public static assembleIds<Id>(childMap: MultiMap<Id>): HCNode<Id>[] { const nodes = new Map<Id, HCNode<Id>>(); const roots: HCNode<Id>[] = []; 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()) { if (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. */ public static assemblePropertyIds(source: object, include?: (prop: string, val: any) => boolean): HCNode<string>[] { 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. */ public static assembleItems<Item, Id>( items: Some<Item>, identify: Identify<Item, Id>, childMap: MultiMap<Id>, ): HCNode<Item>[] { const roots: HCNode<Item>[] = []; const nodeMap = new Map<Id, HCNode<Item>>(); const itemMap = new Map<Id, Item>(); const getItem = (id: 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()) { if (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. */ public static assembleItemsWithChildren<Item>( roots: Some<Item>, children: GetChildren<Item>, ) { const rootNodes = Nodes.createSome(roots); flattenFullGraph({ roots: rootNodes, next: node => { const childItems = children(node.item); if (childItems?.length) { const childNodes = childItems.map(childItem => new HCNode(childItem)); node.attach(childNodes); return childNodes; } }, }); return 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. */ public static assembleItemsWithParents<Item>( leaves: Some<Item>, parent: GetParent<Item>, ) { const nodes = new Map<Item, HCNode<Item>>(); const roots: HCNode<Item>[] = []; for (const leaf of someToIterable(leaves)) { let current: { item: Item, node: HCNode<Item> } | undefined = { item: leaf, node: getNode(leaf), }; while (current) { const parentItem = parent(current.item); if (parentItem !== undefined) { const parentSeen = nodes.has(parentItem); const parentNode = getNode(parentItem); parentNode.attach(current.node); if (parentSeen) break; current = { item: parentItem, node: parentNode, }; } else { roots.push(current.node); current = undefined; } } } return roots; function getNode(item: Item) { let node = nodes.get(item); if (!node) { node = new HCNode(item); nodes.set(item, node); } return 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. */ public static toChildMap<Item, Id = Item>( roots: Some<HCNode<Item>>, identify?: Identify<Item, Id>, childMap = new MultiMap<Id>(), ): MultiMap<Id> { 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(); const nodeId = nodeToId(node, identify); const 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. */ public static toDescendantMap<Item, Id = Item>( roots: Some<HCNode<Item>>, identify?: Identify<Item, Id>, descendantMap = new MultiMap<Id>(), ): MultiMap<Id> { roots = someToArray(roots); type Stored = [ node: HCNode<Item>, ancestors: Set<Id>[] ]; const store = new Queue<Stored>(); store.enqueue(roots.map(node => [ node, [] ] as Stored)); for (const root of roots) descendantMap.getOrAdd(nodeToId(root, identify)); while (store.count > 0) { const [ node, ancestors ] = store.dequeue()!; const nodeId = nodeToId(node, identify); for (const ancestor of ancestors) ancestor.add(nodeId); const children = node.getChildren(); if (children?.length) { const nodeDescendants = descendantMap.getOrAdd(nodeId); const childAncestors = [ ...ancestors, nodeDescendants ]; store.enqueue(children.map(node => [ node, childAncestors ] as Stored)); } } 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. */ public static toAncestorMap<Item, Id = Item>( roots: Some<HCNode<Item>>, identify?: Identify<Item, Id>, ancestorMap = new MultiMap<Id>(), ): MultiMap<Id> { roots = someToArray(roots); type Stored = [ node: HCNode<Item>, ancestors?: Id[] ]; const store = new Queue<Stored>(); store.enqueue(roots.map(node => [ node ] as Stored)); for (const root of roots) { if (root.isLeaf) ancestorMap.getOrAdd(nodeToId(root, identify)); } while (store.count > 0) { const [ node, ancestors ] = store.dequeue()!; const nodeId = nodeToId(node, identify); if (ancestors) ancestorMap.add(nodeId, ancestors); const children = node.getChildren(); if (children?.length) { const childAncestors = ancestors ? [ nodeId, ...ancestors ] : [ nodeId ]; store.enqueue(children.map(node => [ node, childAncestors ] as Stored)); } } 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. */ public static toRelations<Item, Id = Item>( roots: Some<HCNode<Item>>, identify?: Identify<Item, Id>, ): Relation<Id>[] { const relations: Relation<Id>[] = []; flattenFullGraph({ roots, next: node => { if (node.isLeaf) return; const children = node.getChildren(); const childIds = nodesToIds(children, identify); const nodeId = nodeToId(node, identify); for (const childId of childIds) relations.push([ nodeId, childId ]); return children.filter(child => child.isInternal); }, }); return relations; } /** Get unique ancestor nodes. */ public static getAncestors<Item>( nodes: Some<HCNode<Item>>, includeSelf = false, ) { if (isSomeItem(nodes)) return nodes.getAncestors(includeSelf); const seen = new Set<HCNode<Item>>(); const ancestors: HCNode<Item>[] = []; for (const node of someToIterable(nodes)) { const first = includeSelf ? node : node.getParent(); if (!first || seen.has(first)) continue; const unseenAncestors = flattenSequence({ first, next: node => { const parent = node.getParent(); if (!parent || seen.has(parent)) return undefined; seen.add(node); return parent; }, }); ancestors.push(...unseenAncestors); } return ancestors; } /** Get ancestor items from unique ancestor nodes. */ public static getAncestorItems<Item>( nodes: Some<HCNode<Item>>, includeSelf = false, ) { return this.getAncestors(nodes, includeSelf).map(node => node.item); } /** Get descendant nodes by traversing according to the options. */ public static getDescendants<Item>( roots: Some<HCNode<Item>>, includeSelf = false, type: TraversalType = 'breadth-first', ) { return flattenGraph({ roots: HCNode.getRoots(roots, includeSelf), next: node => node.getChildren(), type, }); } /** Get descendant items by traversing according to the options. */ public static getDescendantItems<Item>( roots: Some<HCNode<Item>>, includeSelf = false, type: TraversalType = 'breadth-first', ) { return this.getDescendants(roots, includeSelf, type).map(node => node.item); } /** Generate a sequence of descendant nodes by traversing according to the options. */ public static traverseDescendants<Item>( roots: Some<HCNode<Item>>, includeSelf = false, type: TraversalType = '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`. */ public static findCommonAncestor<Item>(nodes: Some<HCNode<Item>>, includeSelf = false) { const commonAncestors = this.findCommonAncestorSet(nodes, includeSelf); return commonAncestors?.values().next().value; } /** Find the ancestor nodes common to the `nodes`. */ public static findCommonAncestors<Item>(nodes: Some<HCNode<Item>>, includeSelf = false) { const commonAncestors = this.findCommonAncestorSet(nodes, includeSelf); return commonAncestors ? [ ...commonAncestors ] : undefined; } /** Find the ancestor nodes common to the `nodes`. */ public static findCommonAncestorItems<Item>(nodes: Some<HCNode<Item>>, includeSelf = false) { const commonAncestors = this.findCommonAncestorSet(nodes, includeSelf); return commonAncestors ? [ ...commonAncestors ].map(n => n.item) : undefined; } /** Find the set of ancestor nodes common to the `nodes`. */ public static findCommonAncestorSet<Item>(nodes: Some<HCNode<Item>>, includeSelf = false) { const commonAncestors = someToArray(nodes).reduce((acc, curr) => { const ancestors = new Set(curr.getAncestors(includeSelf)); return !acc ? ancestors : acc.intersection(ancestors); }, null as Set<HCNode<Item>> | null); return commonAncestors; } /** Find the first ancestor node matching the `search`. */ public static findAncestor<Item>(roots: Some<HCNode<Item>>, search: NodePredicate<Item>, includeSelf = false) { if (isSomeItem(roots)) return roots.findAncestor(search, includeSelf); const seen = new Set<HCNode<Item>>(); 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 undefined; seen.add(node); return parent; }, search, }); if (found) return found; } return undefined; } /** Find the first ancestor node matching the `search`. */ public static findAncestors<Item>(roots: Some<HCNode<Item>>, search: NodePredicate<Item>, includeSelf = false) { if (isSomeItem(roots)) return roots.findAncestors(search, includeSelf); const seen = new Set<HCNode<Item>>(); const ancestors: HCNode<Item>[] = []; 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 undefined; seen.add(node); return parent; }, search, }); ancestors.push(...found); } return ancestors; } /** Find the first descendant node matching the `search`. */ public static findDescendant<Item>(roots: Some<HCNode<Item>>, search: NodePredicate<Item>, includeSelf = false, type: TraversalType = 'breadth-first') { return searchGraph({ roots: HCNode.getRoots(roots, includeSelf), next: node => node.getChildren(), search, type, }); } /** Find the descendant nodes matching the `search`. */ public static findDescendants<Item>(roots: Some<HCNode<Item>>, search: NodePredicate<Item>, includeSelf = false, type: TraversalType = 'breadth-first') { return searchGraphMany({ roots: HCNode.getRoots(roots, includeSelf), next: node => node.getChildren(), search, type, }); } /** Does an ancestor node matching the `search` exist? */ public static hasAncestor<Item>(roots: Some<HCNode<Item>>, search: NodePredicate<Item>, includeSelf = false) { return this.findAncestor(roots, search, includeSelf) !== undefined; } /** Does a descendant node matching the `search` exist? */ public static hasDescendant<Item>(roots: Some<HCNode<Item>>, search: NodePredicate<Item>, includeSelf = false, type: TraversalType = 'breadth-first') { return this.findDescendant(roots, search, includeSelf, type) !== undefined; } }