UNPKG

@loken/hierarchies

Version:

Library for working with hierarchies of identifiers and identifiable objects.

597 lines (485 loc) 17.7 kB
import { type MapArgs, mapArgs, MultiMap, type Some, someToArray, someToIterable } from '@loken/utilities'; import type { HCNode } from '../nodes/node.js'; import type { DeBrand, NodePredicate } from '../nodes/node.types.js'; import { nodesToIds, nodesToItems } from '../nodes/node-conversion.js'; import { Nodes } from '../nodes/nodes.js'; import { ChildMap } from '../utilities/child-map.js'; import type { Identify } from '../utilities/identify.js'; import type { Relation } from '../utilities/relations.js'; import { Hierarchies } from './hierarchies.js'; /** Contains the `id`, `item` and `node` for a `HCNode` in a `Hierarchy`. */ export type HierarchyEntry<Item, Id> = [id: Id, item: Item, node: HCNode<Item>]; /** * Keeps track of the a set of nodes and their roots * and provides structural modification. */ export class Hierarchy<Item, Id = Item> { /** Create a hierarchy using the provided `identify` function. */ constructor(identify: Identify<Item, Id>) { this.#identify = identify; } //#region backing fields #roots = new Map<Id, HCNode<Item>>(); #nodes = new Map<Id, HCNode<Item>>(); #debrand = new Map<Id, DeBrand>(); #identify: Identify<Item, Id>; //#endregion //#region accessors /** Get a shallow clone of the roots. */ public get roots(): HCNode<Item>[] { return [ ...this.#roots.values() ]; } /** Get a shallow clone of the root items. */ public get rootItems(): Item[] { return this.roots.map(r => r.item); } /** Get a shallow clone of the root IDs. */ public get rootIds(): Id[] { return this.roots.map(r => this.#identify(r.item)); } /** Get a shallow clone of all nodes. */ public get nodes(): HCNode<Item>[] { return [ ...this.#nodes.values() ]; } /** Get a shallow clone of all node items. */ public get nodeItems(): Item[] { return this.nodes.map(n => n.item); } /** Get a shallow clone of all node IDs. */ public get nodeIds(): Id[] { return this.nodes.map(n => this.#identify(n.item)); } /** Means of getting an ID for an `item`. */ public get identify(): Identify<Item, Id> { return this.#identify; } /** Map the `ids` to a boolean signifying their presence. */ public has<Ids extends Id[]>(...ids: Ids): MapArgs<Ids, boolean, true, false> { return mapArgs(ids, id => this.#nodes.has(id), true, false); } /** Is every `Id` in the `ids` present? */ public hasEvery(ids: Some<Id>): boolean { for (const id of someToIterable(ids)) { if (!this.#nodes.has(id)) return false; } return true; } /** Is some `Id` in the `ids` present? */ public hasSome(ids: Some<Id>): boolean { for (const id of someToIterable(ids)) { if (this.#nodes.has(id)) return true; } return false; } /** * Get node or nodes by their `Id`. * @param ids The IDs of nodes to retrieve. * @returns A single node when you pass a single ID and a fixed length tuple of nodes when you pass multiple IDs. * @throws The 'id' must be a hierarchy member. * @throws Must provide at least one argument. */ public get<Ids extends Id[]>(...ids: Ids): MapArgs<Ids, HCNode<Item>, true, false> { return mapArgs(ids, id => this.#get(id), true, false); } /** * Get nodes by their `Id`s. * @param ids The IDs of nodes to retrieve. * @returns An array of nodes. */ public getSome(ids: Some<Id>): HCNode<Item>[] { const nodes: HCNode<Item>[] = []; for (const id of someToIterable(ids)) { const node = this.#nodes.get(id); if (node) nodes.push(node); } return nodes; } /** * Get item or items by `Id`. * @param ids The IDs of items to retrieve. * @throws The 'id' must be a hierarchy member. * @throws Must provide at least one argument. * @returns A single item when you pass a single ID and a fixed length tuple of items when you pass multiple IDs. */ public getItems<Ids extends Id[]>(...ids: Ids): MapArgs<Ids, Item, true, false> { return mapArgs(ids, id => this.#get(id).item, true, false); } /** * Get item or items by `Id`. * @param ids The IDs of items to retrieve. * @returns An array of items. */ public getSomeItems(ids: Some<Id>): Item[] { return this.getSome(ids).map(node => node.item); } #get(id: Id) { const node = this.#nodes.get(id); if (node === undefined) throw new Error("The 'id' must be a hierarchy member."); return node; } //#endregion //#region links /** * Attach the provided `roots`. * @param roots Nodes to attach. */ public attachRoot(roots: Some<HCNode<Item>>): this { const nodes = someToArray(roots); if (!nodes.every(n => n.isRoot)) throw new Error("The 'roots' all be roots!"); this.#addNodes(nodes, true); return this; } /** * Attach the provided `children` to the node of the provided `parentId`. * @param parentId The ID of the node to attach to. * @param children Nodes to attach. */ public attach(parentId: Id, children: Some<HCNode<Item>>): this { if (!this.#nodes.has(parentId)) throw new Error("The 'parentId' must be a hierarchy member."); this.#addNodes(children); const parent = this.#nodes.get(parentId)!; parent.attach(children); return this; } /** * Detach the provided `nodes`. * @param nodes Nodes to detach. */ public detach(nodes: Some<HCNode<Item>>): this { for (const node of Nodes.getDescendants(nodes, true)) { const id = this.#identify(node.item); this.#debrand.get(id)!(); this.#debrand.delete(id); this.#nodes.delete(id); this.#roots.delete(id); } for (const node of someToIterable(nodes)) { if (!node.isRoot) node.detachSelf(); } return this; } #addNodes(nodes: Some<HCNode<Item>>, asRoot = false) { for (const node of Nodes.getDescendants(nodes, true)) { const id = this.#identify(node.item); this.#debrand.set(id, node.brand(this)); this.#nodes.set(id, node); } if (!asRoot) return; for (const node of someToIterable(nodes)) this.#roots.set(this.#identify(node.item), node); } //#endregion //#region traversal /** * Get the chain of ancestor nodes starting with the node for the item matching the `id`. */ public getAncestors(ids: Some<Id>, includeSelf = false): HCNode<Item>[] { const nodes = this.getSome(ids); return nodes ? Nodes.getAncestors(nodes, includeSelf) : []; } /** * Get the items from the chain of ancestor nodes starting with the node for the item matching the `id`. */ public getAncestorItems(ids: Some<Id>, includeSelf = false): Item[] { return nodesToItems(this.getAncestors(ids, includeSelf)); } /** * Get the IDs from the chain of ancestor nodes starting with the node for the item matching the `id`. */ public getAncestorIds(ids: Some<Id>, includeSelf = false): Id[] { return nodesToIds(this.getAncestors(ids, includeSelf), this.#identify); } /** * Get the entries from the chain of ancestor nodes starting with the node for the item matching the `id`. */ public getAncestorEntries(id: Id, includeSelf = false): HierarchyEntry<Item, Id>[] { return this.getAncestors(id, includeSelf).map(node => { return [ this.#identify(node.item), node.item, node ]; }); } /** * Get the chain of descendant nodes starting with the nodes for the items matching the `ids`. */ public getDescendants(ids: Some<Id>, includeSelf = false): HCNode<Item>[] { const roots = this.getSome(ids); if (roots.length === 0) return []; return Nodes.getDescendants(roots, includeSelf); } /** * Get the items from the chain of descendant nodes starting with the nodes for the items matching the `ids`. */ public getDescendantItems(ids: Some<Id>, includeSelf = false): Item[] { return nodesToItems(this.getDescendants(ids, includeSelf)); } /** * Get the IDs from the chain of descendant nodes starting with the nodes for the items matching the `ids`. */ public getDescendantIds(ids: Some<Id>, includeSelf = false): Id[] { return nodesToIds(this.getDescendants(ids, includeSelf), this.#identify); } /** * Get the entries from the chain of descendant nodes starting with the nodes for the items matching the `ids`. */ public getDescendantEntries(ids: Some<Id>, includeSelf = false): HierarchyEntry<Item, Id>[] { return this.getDescendants(ids, includeSelf).map(node => { return [ this.#identify(node.item), node.item, node ]; }); } /** * Get the chain of descendant nodes starting with the hierarchy `roots`. */ public getAllDescendants(includeSelf = false): HCNode<Item>[] { return Nodes.getDescendants(this.roots, includeSelf); } /** * Get the items from the chain of descendant nodes starting with the hierarchy `roots`. */ public getAllDescendantItems(includeSelf = false): Item[] { return nodesToItems(Nodes.getDescendants(this.roots, includeSelf)); } /** * Get the IDs from the chain of descendant nodes starting with the hierarchy `roots`. */ public getAllDescendantIds(includeSelf = false): Id[] { return nodesToIds(Nodes.getDescendants(this.roots, includeSelf), this.#identify); } /** * Get the entries from the chain of descendant nodes starting with the hierarchy `roots`. */ public getAllDescendantEntries(includeSelf = false): HierarchyEntry<Item, Id>[] { return Nodes.getDescendants(this.roots, includeSelf).map(node => { return [ this.#identify(node.item), node.item, node ]; }); } //#endregion //#region search /** * Find nodes matching a list of `Id`s or a `HCNode<Item>` predicate. */ public find(search: Some<Id> | NodePredicate<Item>): HCNode<Item>[] { if (typeof search === 'function') { const result: HCNode<Item>[] = []; for (const node of this.#nodes.values()) { if ((search as NodePredicate<Item>)(node)) result.push(node); } return result; } return this.getSome(search); } /** * Find nodes matching a list of `Id`s or a `HCNode<Item>` predicate. */ public findItems(search: Some<Id> | NodePredicate<Item>): Item[] { return this.find(search).map(node => node.item); } /** * Find `Id`s matching a list of `Id`s or a `HCNode<Item>` predicate. */ public findIds(search: Some<Id> | NodePredicate<Item>): Id[] { const result: Id[] = []; if (typeof search === 'function') { for (const [ id, node ] of this.#nodes) { if ((search as NodePredicate<Item>)(node)) result.push(id); } } else { for (const id of someToIterable(search)) { if (this.#nodes.has(id)) result.push(id); } } return result; } /** * Find entries matching a list of `Id`s or a `HCNode<Item>` predicate. */ public findEntries(search: Some<Id> | NodePredicate<Item>): HierarchyEntry<Item, Id>[] { const result: HierarchyEntry<Item, Id>[] = []; if (typeof search === 'function') { for (const node of this.#nodes.values()) { if ((search as NodePredicate<Item>)(node)) result.push([ this.#identify(node.item), node.item, node ]); } } else { for (const id of someToIterable(search)) { const node = this.#nodes.get(id); if (node) result.push([ id, node.item, node ]); } } return result; } /** Find the common ancestor node which is the closest to the `ids`. */ public findCommonAncestor(ids: Some<Id>, includeSelf = false): HCNode<Item> | undefined { const nodes = this.getSome(ids); return Nodes.findCommonAncestor(nodes, includeSelf); } /** Find the ancestor nodes common to the `ids`. */ public findCommonAncestors(ids: Some<Id>, includeSelf = false): HCNode<Item>[] | undefined { const nodes = this.getSome(ids); return Nodes.findCommonAncestors(nodes, includeSelf); } /** * Find a node matching the `search` which is an ancestor of a node with one of the `ids`. */ public findAncestor(ids: Some<Id>, search: Some<Id> | NodePredicate<Item>, includeSelf = false): HCNode<Item> | undefined { const roots = this.getSome(ids); return Nodes.findAncestor(roots, this.normalizeSearch(search), includeSelf); } /** * Find a node matching the `search` which is an ancestor of a node with one of the `ids`. */ public findAncestors(ids: Some<Id>, search: Some<Id> | NodePredicate<Item>, includeSelf = false): HCNode<Item>[] { const roots = this.getSome(ids); return Nodes.findAncestors(roots, this.normalizeSearch(search), includeSelf); } /** * Find a node matching the `search` which is an descendant of a node with one of the `ids`. */ public findDescendant(ids: Some<Id>, search: Some<Id> | NodePredicate<Item>, includeSelf = false): HCNode<Item> | undefined { const roots = this.getSome(ids); return Nodes.findDescendant(roots, this.normalizeSearch(search), includeSelf); } /** * Find nodes matching the `search` which are descendants of a node with one of the `ids`. */ public findDescendants(ids: Some<Id>, search: Some<Id> | NodePredicate<Item>, includeSelf = false): HCNode<Item>[] { const roots = this.getSome(ids); return Nodes.findDescendants(roots, this.normalizeSearch(search), includeSelf); } /** * Does a node with one of the `ids` have an ancestor node matching the `search`? */ public hasAncestor(ids: Some<Id>, search: Some<Id> | NodePredicate<Item>, includeSelf = false): boolean { const roots = this.getSome(ids); return Nodes.hasAncestor(roots, this.normalizeSearch(search), includeSelf); } /** * Does a node with one of the `ids` have a descendant node matching the `search`? */ public hasDescendant(ids: Some<Id>, search: Some<Id> | NodePredicate<Item>, includeSelf = false): boolean { const roots = this.getSome(ids); return Nodes.hasDescendant(roots, this.normalizeSearch(search), includeSelf); } /** * Create a new `Hierarchy` from matching items. * * @param search A list of `Id`s or a `HCNode<Item>` predicate. * @param include Optional facets to include: The nodes that are `matches`, their `ancestors` and/or their `descendants` in the result. * - When not specified you are getting all three facets as a default. * - When the options object is specified you must opt in to the facets you want and must enable at least one. * @returns A new `Hierarchy<Item, Id>` with new nodes wrapping the same `Item`s as in the searched hierarchy pruned to fit the search and `include` facets. * @throws If you provide `include` options but enable no facets. */ public search( search: Some<Id> | NodePredicate<Item>, include?: { matches?: boolean, ancestors?: boolean, descendants?: boolean }, ): Hierarchy<Item, Id> { include ??= { matches: true, ancestors: true, descendants: true, }; if (!(include.matches || include.ancestors || include.descendants)) throw new Error("Must enable at least one facet to 'include'."); const childMap = new MultiMap<Id>(); const items = new Map<Id, Item>(); for (const [ id, item, node ] of this.findEntries(search)) { if (include.ancestors) { const ancestorIds: Id[] = []; for (const [ ancestorId, ancestorItem ] of this.getAncestorEntries(id, include.matches)) { ancestorIds.push(ancestorId); if (!items.has(ancestorId)) items.set(ancestorId, ancestorItem); } ChildMap.addAncestors(ancestorIds, childMap); } if (include.descendants) { for (const [ descendantId, descendantItem, descendantNode ] of this.getDescendantEntries(id, include.matches)) { if (!items.has(descendantId)) { items.set(descendantId, descendantItem); if (descendantNode.isLeaf) childMap.getOrAdd(descendantId); } if (!descendantNode.isLeaf) { for (const childNode of descendantNode.getChildren()) { const childItem = childNode.item; const childId = this.#identify(childItem); childMap.add(descendantId, childId); items.set(childId, childItem); } } } } if (include.matches && !include.ancestors && !include.descendants) { let addedToChildMap = false; if (!node.isRoot) { const parentId = this.#identify(node.getParentItem()!); if (items.has(parentId)) { childMap.add(parentId, id); addedToChildMap = true; } } if (!node.isLeaf) { const includedChildIds = node .getChildItems() .map(this.#identify) .filter(id => items.has(id)); if (includedChildIds.length) { childMap.add(id, includedChildIds); addedToChildMap = true; } } if (!addedToChildMap) childMap.getOrAdd(id); items.set(id, item); } } return Hierarchies.createWithItems({ items: [ ...items.values() ], identify: this.#identify, spec: childMap, }); } protected normalizeSearch(search: Some<Id> | NodePredicate<Item>): NodePredicate<Item> { if (typeof search === 'function') return search as NodePredicate<Item>; if (Array.isArray(search)) return (node) => search.includes(this.#identify(node.item)); else if (search instanceof Set) return (node) => search.has(this.#identify(node.item)); else return (node) => this.#identify(node.item) === search; } //#endregion //#region MultiMaps /** Create a map of ids to child-ids by traversing the `hierarchy`. */ public toChildMap(): MultiMap<Id> { return Nodes.toChildMap(this.roots, this.#identify); } /** Create a map of ids to descendant-ids by traversing the `hierarchy`. */ public toDescendantMap(): MultiMap<Id> { return Nodes.toDescendantMap(this.roots, this.#identify); } /** Create a map of ids to ancestor-ids by traversing the `hierarchy`. */ public toAncestorMap(): MultiMap<Id> { return Nodes.toAncestorMap(this.roots, this.#identify); } /** Create a list of relations by traversing the graph of the `hierarchy`. */ public toRelations(): Relation<Id>[] { return Nodes.toRelations(this.roots, this.#identify); } //#endregion }