UNPKG

hyperformula

Version:

HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas

339 lines 10.5 kB
/** * @license * Copyright (c) 2025 Handsoncode. All rights reserved. */ import { TopSort } from "./TopSort.mjs"; import { ProcessableValue } from "./ProcessableValue.mjs"; /** * Provides directed graph structure. * * Idea for performance improvement: * - use Set<Node>[] instead of NodeId[][] for edgesSparseArray */ export class Graph { constructor(dependencyQuery) { this.dependencyQuery = dependencyQuery; /** * A sparse array. The value nodesSparseArray[n] exists if and only if node n is in the graph. * @private */ this.nodesSparseArray = []; /** * A sparse array. The value edgesSparseArray[n] exists if and only if node n is in the graph. * The edgesSparseArray[n] is also a sparse array. It may contain removed nodes. To make sure check nodesSparseArray. * @private */ this.edgesSparseArray = []; /** * A mapping from node to its id. The value nodesIds.get(node) exists if and only if node is in the graph. * @private */ this.nodesIds = new Map(); /** * A ProcessableValue object. * @private */ this.dirtyAndVolatileNodeIds = new ProcessableValue({ dirty: [], volatile: [] }, r => this.processDirtyAndVolatileNodeIds(r)); /** * A set of node ids. The value infiniteRangeIds.get(nodeId) exists if and only if node is in the graph. * @private */ this.infiniteRangeIds = new Set(); /** * A dense array. It may contain duplicates and removed nodes. * @private */ this.changingWithStructureNodeIds = []; this.nextId = 0; } /** * Iterate over all nodes the in graph */ getNodes() { return this.nodesSparseArray.filter(node => node !== undefined); } /** * Checks whether a node is present in graph * * @param node - node to check */ hasNode(node) { return this.nodesIds.has(node); } /** * Checks whether exists edge between nodes. If one or both of nodes are not present in graph, returns false. * * @param fromNode - node from which edge is outcoming * @param toNode - node to which edge is incoming */ existsEdge(fromNode, toNode) { const fromId = this.getNodeId(fromNode); const toId = this.getNodeId(toNode); if (fromId === undefined || toId === undefined) { return false; } return this.edgesSparseArray[fromId].includes(toId); } /** * Returns nodes adjacent to given node. May contain removed nodes. * * @param node - node to which adjacent nodes we want to retrieve * * Idea for performance improvement: * - return an array instead of set */ adjacentNodes(node) { const id = this.getNodeId(node); if (id === undefined) { throw this.missingNodeError(node); } return new Set(this.edgesSparseArray[id].filter(id => id !== undefined).map(id => this.nodesSparseArray[id])); } /** * Returns number of nodes adjacent to given node. Contrary to adjacentNodes(), this method returns only nodes that are present in graph. * * @param node - node to which adjacent nodes we want to retrieve */ adjacentNodesCount(node) { const id = this.getNodeId(node); if (id === undefined) { throw this.missingNodeError(node); } return this.fixEdgesArrayForNode(id).length; } /** * Adds node to a graph * * @param node - a node to be added */ addNodeAndReturnId(node) { const idOfExistingNode = this.nodesIds.get(node); if (idOfExistingNode !== undefined) { return idOfExistingNode; } const newId = this.nextId; this.nextId++; this.nodesSparseArray[newId] = node; this.edgesSparseArray[newId] = []; this.nodesIds.set(node, newId); return newId; } /** * Adds edge between nodes. * * The nodes had to be added to the graph before, or the error will be raised * * @param fromNode - node from which edge is outcoming * @param toNode - node to which edge is incoming */ addEdge(fromNode, toNode) { const fromId = this.getNodeIdIfNotNumber(fromNode); const toId = this.getNodeIdIfNotNumber(toNode); if (fromId === undefined) { throw this.missingNodeError(fromNode); } if (toId === undefined) { throw this.missingNodeError(toNode); } if (this.edgesSparseArray[fromId].includes(toId)) { return; } this.edgesSparseArray[fromId].push(toId); } /** * Removes node from graph */ removeNode(node) { const id = this.getNodeId(node); if (id === undefined) { throw this.missingNodeError(node); } if (this.edgesSparseArray[id].length > 0) { this.edgesSparseArray[id].forEach(adjacentId => this.dirtyAndVolatileNodeIds.rawValue.dirty.push(adjacentId)); this.dirtyAndVolatileNodeIds.markAsModified(); } const dependencies = this.removeDependencies(node); delete this.nodesSparseArray[id]; delete this.edgesSparseArray[id]; this.infiniteRangeIds.delete(id); this.nodesIds.delete(node); return dependencies; } /** * Removes edge between nodes. */ removeEdge(fromNode, toNode) { const fromId = this.getNodeIdIfNotNumber(fromNode); const toId = this.getNodeIdIfNotNumber(toNode); if (fromId === undefined) { throw this.missingNodeError(fromNode); } if (toId === undefined) { throw this.missingNodeError(toNode); } const indexOfToId = this.edgesSparseArray[fromId].indexOf(toId); if (indexOfToId === -1) { throw new Error('Edge does not exist'); } delete this.edgesSparseArray[fromId][indexOfToId]; } /** * Removes edge between nodes if it exists. */ removeEdgeIfExists(fromNode, toNode) { const fromId = this.getNodeIdIfNotNumber(fromNode); const toId = this.getNodeIdIfNotNumber(toNode); if (fromId === undefined) { return; } if (toId === undefined) { return; } const indexOfToId = this.edgesSparseArray[fromId].indexOf(toId); if (indexOfToId === -1) { return; } delete this.edgesSparseArray[fromId][indexOfToId]; } /** * Sorts the whole graph topologically. Nodes that are on cycles are kept separate. */ topSortWithScc() { return this.getTopSortedWithSccSubgraphFrom(this.getNodes(), () => true, () => {}); } /** * Sorts the graph topologically. Nodes that are on cycles are kept separate. * * @param modifiedNodes - seed for computation. The algorithm assumes that only these nodes have changed since the last run. * @param operatingFunction - recomputes value of a node, and returns whether a change occurred * @param onCycle - action to be performed when node is on cycle */ getTopSortedWithSccSubgraphFrom(modifiedNodes, operatingFunction, onCycle) { const topSortAlgorithm = new TopSort(this.nodesSparseArray, this.edgesSparseArray); const modifiedNodesIds = modifiedNodes.map(node => this.getNodeId(node)).filter(id => id !== undefined); return topSortAlgorithm.getTopSortedWithSccSubgraphFrom(modifiedNodesIds, operatingFunction, onCycle); } /** * Marks node as volatile. */ markNodeAsVolatile(node) { const id = this.getNodeId(node); if (id === undefined) { return; } this.dirtyAndVolatileNodeIds.rawValue.volatile.push(id); this.dirtyAndVolatileNodeIds.markAsModified(); } /** * Marks node as dirty. */ markNodeAsDirty(node) { const id = this.getNodeId(node); if (id === undefined) { return; } this.dirtyAndVolatileNodeIds.rawValue.dirty.push(id); this.dirtyAndVolatileNodeIds.markAsModified(); } /** * Returns an array of nodes that are marked as dirty and/or volatile. */ getDirtyAndVolatileNodes() { return this.dirtyAndVolatileNodeIds.getProcessedValue(); } /** * Clears dirty nodes. */ clearDirtyNodes() { this.dirtyAndVolatileNodeIds.rawValue.dirty = []; this.dirtyAndVolatileNodeIds.markAsModified(); } /** * Marks node as changingWithStructure. */ markNodeAsChangingWithStructure(node) { const id = this.getNodeId(node); if (id === undefined) { return; } this.changingWithStructureNodeIds.push(id); } /** * Marks all nodes marked as changingWithStructure as dirty. */ markChangingWithStructureNodesAsDirty() { if (this.changingWithStructureNodeIds.length <= 0) { return; } this.dirtyAndVolatileNodeIds.rawValue.dirty = [...this.dirtyAndVolatileNodeIds.rawValue.dirty, ...this.changingWithStructureNodeIds]; this.dirtyAndVolatileNodeIds.markAsModified(); } /** * Marks node as infinite range. */ markNodeAsInfiniteRange(node) { const id = this.getNodeIdIfNotNumber(node); if (id === undefined) { return; } this.infiniteRangeIds.add(id); } /** * Returns an array of nodes marked as infinite ranges */ getInfiniteRanges() { return [...this.infiniteRangeIds].map(id => ({ node: this.nodesSparseArray[id], id })); } /** * Returns the internal id of a node. */ getNodeId(node) { return this.nodesIds.get(node); } /** * */ getNodeIdIfNotNumber(node) { return typeof node === 'number' ? node : this.nodesIds.get(node); } /** * Removes invalid neighbors of a given node from the edges array and returns adjacent nodes for the input node. */ fixEdgesArrayForNode(id) { const adjacentNodeIds = this.edgesSparseArray[id]; this.edgesSparseArray[id] = adjacentNodeIds.filter(adjacentId => adjacentId !== undefined && this.nodesSparseArray[adjacentId]); return this.edgesSparseArray[id]; } /** * Removes edges from the given node to its dependencies based on the dependencyQuery function. */ removeDependencies(node) { const dependencies = this.dependencyQuery(node); dependencies.forEach(([_, dependency]) => { this.removeEdgeIfExists(dependency, node); }); return dependencies; } /** * processFn for dirtyAndVolatileNodeIds ProcessableValue instance * @private */ processDirtyAndVolatileNodeIds({ dirty, volatile }) { return [...new Set([...dirty, ...volatile])].map(id => this.nodesSparseArray[id]).filter(node => node !== undefined); } /** * Returns error for missing node. */ missingNodeError(node) { return new Error(`Unknown node ${node}`); } }