hyperformula
Version:
HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas
1,195 lines • 48.4 kB
JavaScript
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/
import { AbsoluteCellRange, simpleCellRange } from "../AbsoluteCellRange.mjs";
import { absolutizeDependencies } from "../absolutizeDependencies.mjs";
import { CellError, ErrorType, isSimpleCellAddress, simpleCellAddress } from "../Cell.mjs";
import { ContentChanges } from "../ContentChanges.mjs";
import { ErrorMessage } from "../error-message.mjs";
import { EmptyValue, getRawValue } from "../interpreter/InterpreterValue.mjs";
import { SimpleRangeValue } from "../SimpleRangeValue.mjs";
import { NamedExpressions } from "../NamedExpressions.mjs";
import { collectDependencies, NamedExpressionDependency } from "../parser/index.mjs";
import { ColumnsSpan, RowsSpan } from "../Span.mjs";
import { StatType } from "../statistics/index.mjs";
import { ArrayFormulaVertex, EmptyCellVertex, ScalarFormulaVertex, RangeVertex, ValueCellVertex } from ".//index.mjs";
import { AddressMapping } from "./AddressMapping/AddressMapping.mjs";
import { ArrayMapping } from "./ArrayMapping.mjs";
import { collectAddressesDependentToRange } from "./collectAddressesDependentToRange.mjs";
import { FormulaVertex } from "./FormulaVertex.mjs";
import { Graph } from "./Graph.mjs";
import { RangeMapping } from "./RangeMapping.mjs";
import { SheetMapping } from "./SheetMapping.mjs";
import { SheetReferenceRegistrar } from "./SheetReferenceRegistrar.mjs";
import { findBoundaries } from "../Sheet.mjs";
export class DependencyGraph {
constructor(addressMapping, rangeMapping, sheetMapping, arrayMapping, stats, lazilyTransformingAstService, functionRegistry, namedExpressions) {
this.addressMapping = addressMapping;
this.rangeMapping = rangeMapping;
this.sheetMapping = sheetMapping;
this.arrayMapping = arrayMapping;
this.stats = stats;
this.lazilyTransformingAstService = lazilyTransformingAstService;
this.functionRegistry = functionRegistry;
this.namedExpressions = namedExpressions;
this.changes = ContentChanges.empty();
this.dependencyQueryAddresses = vertex => {
if (vertex instanceof RangeVertex) {
return this.rangeDependencyQuery(vertex).map(([address, _]) => address);
} else {
const dependenciesResult = this.formulaDependencyQuery(vertex);
if (dependenciesResult !== undefined) {
const [address, dependencies] = dependenciesResult;
return dependencies.map(dependency => {
if (dependency instanceof NamedExpressionDependency) {
return this.namedExpressions.namedExpressionOrPlaceholder(dependency.name, address.sheet).address;
} else if (isSimpleCellAddress(dependency)) {
return dependency;
} else {
return simpleCellRange(dependency.start, dependency.end);
}
});
} else {
return [];
}
}
};
this.dependencyQueryVertices = vertex => {
if (vertex instanceof RangeVertex) {
return this.rangeDependencyQuery(vertex);
} else {
const dependenciesResult = this.formulaDependencyQuery(vertex);
if (dependenciesResult !== undefined) {
const [address, dependencies] = dependenciesResult;
return dependencies.map(dependency => {
if (dependency instanceof AbsoluteCellRange) {
return [dependency.start, this.rangeMapping.getVertexOrThrow(dependency.start, dependency.end)];
} else if (dependency instanceof NamedExpressionDependency) {
const namedExpression = this.namedExpressions.namedExpressionOrPlaceholder(dependency.name, address.sheet);
return [namedExpression.address, this.addressMapping.getCellOrThrow(namedExpression.address)];
} else {
return [dependency, this.addressMapping.getCellOrThrow(dependency)];
}
});
} else {
return [];
}
}
};
this.rangeDependencyQuery = vertex => {
const allDeps = [];
const {
smallerRangeVertex,
restRange
} = this.rangeMapping.findSmallerRange(vertex.range); //checking whether this range was splitted by bruteForce or not
let range;
if (smallerRangeVertex !== undefined && this.graph.adjacentNodes(smallerRangeVertex).has(vertex)) {
range = restRange;
allDeps.push([new AbsoluteCellRange(smallerRangeVertex.start, smallerRangeVertex.end), smallerRangeVertex]);
} else {
//did we ever need to use full range
range = vertex.range;
}
for (const address of range.addresses(this)) {
const cell = this.addressMapping.getCell(address);
if (cell !== undefined) {
allDeps.push([address, cell]);
}
}
return allDeps;
};
this.formulaDependencyQuery = vertex => {
let formula;
let address;
if (vertex instanceof FormulaVertex) {
address = vertex.getAddress(this.lazilyTransformingAstService);
formula = vertex.getFormula(this.lazilyTransformingAstService);
} else {
return undefined;
}
const deps = collectDependencies(formula, this.functionRegistry);
return [address, absolutizeDependencies(deps, address)];
};
this.graph = new Graph(this.dependencyQueryVertices);
this.sheetReferenceRegistrar = new SheetReferenceRegistrar(sheetMapping, addressMapping);
}
/**
* Invariants:
* - empty cell has associated EmptyCellVertex if and only if it is a dependency (possibly indirect, through range) to some formula
*/
static buildEmpty(lazilyTransformingAstService, config, functionRegistry, namedExpressions, stats) {
return new DependencyGraph(new AddressMapping(config.chooseAddressMappingPolicy), new RangeMapping(), new SheetMapping(config.translationPackage), new ArrayMapping(), stats, lazilyTransformingAstService, functionRegistry, namedExpressions);
}
setFormulaToCell(address, ast, dependencies, size, hasVolatileFunction, hasStructuralChangeFunction) {
const newVertex = FormulaVertex.fromAst(ast, address, size, this.lazilyTransformingAstService.version());
this.exchangeOrAddFormulaVertex(newVertex);
this.processCellDependencies(dependencies, newVertex);
this.graph.markNodeAsDirty(newVertex);
if (hasVolatileFunction) {
this.markAsVolatile(newVertex);
}
if (hasStructuralChangeFunction) {
this.markAsDependentOnStructureChange(newVertex);
}
this.correctInfiniteRangesDependency(address);
return this.getAndClearContentChanges();
}
setParsingErrorToCell(address, errorVertex) {
const vertex = this.shrinkPossibleArrayAndGetCell(address);
this.exchangeOrAddGraphNode(vertex, errorVertex);
this.addressMapping.setCell(address, errorVertex);
this.graph.markNodeAsDirty(errorVertex);
this.correctInfiniteRangesDependency(address);
return this.getAndClearContentChanges();
}
setValueToCell(address, value) {
const vertex = this.shrinkPossibleArrayAndGetCell(address);
if (vertex instanceof ArrayFormulaVertex) {
this.arrayMapping.removeArray(vertex.getRange());
}
if (vertex instanceof ValueCellVertex) {
const oldValues = vertex.getValues();
if (oldValues.rawValue !== value.rawValue) {
vertex.setValues(value);
this.graph.markNodeAsDirty(vertex);
}
} else {
const newVertex = new ValueCellVertex(value.parsedValue, value.rawValue);
this.exchangeOrAddGraphNode(vertex, newVertex);
this.addressMapping.setCell(address, newVertex);
this.graph.markNodeAsDirty(newVertex);
}
this.correctInfiniteRangesDependency(address);
return this.getAndClearContentChanges();
}
/**
* Sets a cell empty.
* - if vertex has no dependents, removes it from graph, address mapping and range mapping and cleans up its dependencies
* - if vertex has dependents, exchanges it for an EmptyCellVertex and marks it as dirty
*/
setCellEmpty(address) {
const vertex = this.shrinkPossibleArrayAndGetCell(address);
if (vertex === undefined) {
return ContentChanges.empty();
}
if (this.graph.adjacentNodes(vertex).size > 0) {
const emptyVertex = new EmptyCellVertex();
this.exchangeGraphNode(vertex, emptyVertex);
if (this.graph.adjacentNodesCount(emptyVertex) === 0) {
this.removeVertex(emptyVertex);
this.addressMapping.removeCell(address);
} else {
this.graph.markNodeAsDirty(emptyVertex);
this.addressMapping.setCell(address, emptyVertex);
}
} else {
this.removeVertex(vertex);
this.addressMapping.removeCell(address);
}
return this.getAndClearContentChanges();
}
clearDirtyVertices() {
this.graph.clearDirtyNodes();
}
verticesToRecompute() {
return this.graph.getDirtyAndVolatileNodes();
}
processCellDependencies(cellDependencies, endVertex) {
const endVertexId = this.graph.getNodeId(endVertex);
if (endVertexId === undefined) {
throw new Error('End vertex not found');
}
cellDependencies.forEach(dep => {
if (dep instanceof AbsoluteCellRange) {
const range = dep;
let rangeVertex = this.getRange(range.start, range.end);
if (rangeVertex === undefined) {
rangeVertex = new RangeVertex(range);
this.rangeMapping.addOrUpdateVertex(rangeVertex);
}
this.graph.addNodeAndReturnId(rangeVertex);
const rangeVertexId = this.graph.getNodeId(rangeVertex);
if (rangeVertexId === undefined) {
throw new Error('Range vertex not found');
}
if (!range.isFinite()) {
this.graph.markNodeAsInfiniteRange(rangeVertexId);
}
const {
smallerRangeVertex,
restRange
} = this.rangeMapping.findSmallerRange(range);
if (smallerRangeVertex !== undefined) {
this.graph.addEdge(smallerRangeVertex, rangeVertexId);
if (rangeVertex.bruteForce) {
rangeVertex.bruteForce = false;
for (const cellFromRange of range.addresses(this)) {
//if we ever switch heuristic to processing by sorted sizes, this would be unnecessary
this.graph.removeEdge(this.fetchCell(cellFromRange), rangeVertexId);
}
}
} else {
rangeVertex.bruteForce = true;
}
const array = this.arrayMapping.getArray(restRange);
if (array !== undefined) {
this.graph.addEdge(array, rangeVertexId);
} else {
for (const cellFromRange of restRange.addresses(this)) {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(cellFromRange);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, rangeVertexId);
}
}
this.graph.addEdge(rangeVertexId, endVertexId);
if (range.isFinite()) {
this.correctInfiniteRangesDependenciesByRangeVertex(rangeVertex);
}
} else if (dep instanceof NamedExpressionDependency) {
const sheetOfVertex = endVertex.getAddress(this.lazilyTransformingAstService).sheet;
const {
vertex,
id
} = this.fetchNamedExpressionVertex(dep.name, sheetOfVertex);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, endVertexId);
} else {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(dep);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, endVertexId);
}
});
}
fetchNamedExpressionVertex(expressionName, sheetId) {
const namedExpression = this.namedExpressions.namedExpressionOrPlaceholder(expressionName, sheetId);
return this.fetchCellOrCreateEmpty(namedExpression.address);
}
exchangeNode(addressFrom, addressTo) {
const vertexFrom = this.fetchCellOrCreateEmpty(addressFrom).vertex;
const vertexTo = this.fetchCellOrCreateEmpty(addressTo).vertex;
this.addressMapping.removeCell(addressFrom);
this.exchangeGraphNode(vertexFrom, vertexTo);
}
fetchCellOrCreateEmpty(address) {
const existingVertex = this.addressMapping.getCell(address);
if (existingVertex !== undefined) {
return {
vertex: existingVertex,
id: undefined
};
}
const newVertex = new EmptyCellVertex();
const newVertexId = this.graph.addNodeAndReturnId(newVertex);
this.addressMapping.setCell(address, newVertex);
return {
vertex: newVertex,
id: newVertexId
};
}
removeRows(removedRows) {
this.stats.measure(StatType.ADJUSTING_GRAPH, () => {
for (const [address, vertex] of this.addressMapping.entriesFromRowsSpan(removedRows)) {
for (const adjacentNode of this.graph.adjacentNodes(vertex)) {
this.graph.markNodeAsDirty(adjacentNode);
}
if (vertex instanceof ArrayFormulaVertex) {
if (vertex.isLeftCorner(address)) {
this.shrinkArrayToCorner(vertex);
this.arrayMapping.removeArray(vertex.getRange());
} else {
continue;
}
}
this.removeVertex(vertex);
}
});
this.stats.measure(StatType.ADJUSTING_ADDRESS_MAPPING, () => {
this.addressMapping.removeRows(removedRows);
});
const affectedArrays = this.stats.measure(StatType.ADJUSTING_RANGES, () => {
const affectedRanges = this.truncateRanges(removedRows, address => address.row);
return this.getArrayVerticesRelatedToRanges(affectedRanges);
});
this.stats.measure(StatType.ADJUSTING_ARRAY_MAPPING, () => {
this.fixArraysAfterRemovingRows(removedRows.sheet, removedRows.rowStart, removedRows.numberOfRows);
});
this.addStructuralNodesToChangeSet();
return {
affectedArrays,
contentChanges: this.getAndClearContentChanges()
};
}
/**
* Adds a new sheet to the graph.
* If the sheetId was a placeholder sheet, marks its vertices as dirty.
*/
addSheet(sheetId) {
this.addressMapping.addSheetOrChangeStrategy(sheetId, findBoundaries([]));
this.stats.measure(StatType.ADJUSTING_ADDRESS_MAPPING, () => {
this.markAllCellsAsDirtyInSheet(sheetId);
});
this.stats.measure(StatType.ADJUSTING_RANGES, () => {
this.markAllRangesAsDirtyInSheet(sheetId);
});
}
/**
* Removes all vertices without dependents in other sheets from address mapping, range mapping and array mapping.
* - If nothing is left, removes the sheet from sheet mapping and address mapping.
* - Otherwise, marks it as placeholder.
*/
removeSheet(sheetId) {
this.clearSheet(sheetId);
const addressMappingCleared = !this.addressMapping.hasAnyEntries(sheetId);
const rangeMappingCleared = this.rangeMapping.getNumberOfRangesInSheet(sheetId) === 0;
if (addressMappingCleared && rangeMappingCleared) {
this.sheetMapping.removeSheetIfExists(sheetId);
this.addressMapping.removeSheetIfExists(sheetId);
} else {
this.sheetMapping.markSheetAsPlaceholder(sheetId);
}
}
/**
* Removes placeholderSheetToDelete and reroutes edges to the corresponding vertices in sheetToKeep
*
* Assumptions about placeholderSheetToDelete:
* - is empty (contains only empty cell vertices and range vertices),
* - empty cell vertices have no dependencies,
* - range vertices have dependencies only in placeholderSheetToDelete,
* - vertices may have dependents in placeholderSheetToDelete and other sheets,
*/
mergeSheets(sheetToKeep, placeholderSheetToDelete) {
if (!this.isPlaceholder(placeholderSheetToDelete)) {
throw new Error(`Cannot merge sheets: sheet ${placeholderSheetToDelete} is not a placeholder`);
}
this.mergeRangeVertices(sheetToKeep, placeholderSheetToDelete);
this.mergeCellVertices(sheetToKeep, placeholderSheetToDelete);
this.addressMapping.removeSheetIfExists(placeholderSheetToDelete);
this.addStructuralNodesToChangeSet();
}
/**
* Clears the sheet content.
* - removes all cell vertices without dependents
* - removes all array vertices
* - for vertices with dependents, exchanges them for EmptyCellVertex and marks them as dirty
*/
clearSheet(sheetId) {
const arrays = new Set();
for (const [address, vertex] of this.addressMapping.sheetEntries(sheetId)) {
if (vertex instanceof ArrayFormulaVertex) {
arrays.add(vertex);
} else {
this.setCellEmpty(address);
}
}
for (const array of arrays.values()) {
this.setArrayEmpty(array);
}
this.addStructuralNodesToChangeSet();
}
removeColumns(removedColumns) {
this.stats.measure(StatType.ADJUSTING_GRAPH, () => {
for (const [address, vertex] of this.addressMapping.entriesFromColumnsSpan(removedColumns)) {
for (const adjacentNode of this.graph.adjacentNodes(vertex)) {
this.graph.markNodeAsDirty(adjacentNode);
}
if (vertex instanceof ArrayFormulaVertex) {
if (vertex.isLeftCorner(address)) {
this.shrinkArrayToCorner(vertex);
this.arrayMapping.removeArray(vertex.getRange());
} else {
continue;
}
}
this.removeVertex(vertex);
}
});
this.stats.measure(StatType.ADJUSTING_ADDRESS_MAPPING, () => {
this.addressMapping.removeColumns(removedColumns);
});
const affectedArrays = this.stats.measure(StatType.ADJUSTING_RANGES, () => {
const affectedRanges = this.truncateRanges(removedColumns, address => address.col);
return this.getArrayVerticesRelatedToRanges(affectedRanges);
});
this.stats.measure(StatType.ADJUSTING_ARRAY_MAPPING, () => {
return this.fixArraysAfterRemovingColumns(removedColumns.sheet, removedColumns.columnStart, removedColumns.numberOfColumns);
});
this.addStructuralNodesToChangeSet();
return {
affectedArrays,
contentChanges: this.getAndClearContentChanges()
};
}
addRows(addedRows) {
this.stats.measure(StatType.ADJUSTING_ADDRESS_MAPPING, () => {
this.addressMapping.addRows(addedRows.sheet, addedRows.rowStart, addedRows.numberOfRows);
});
const affectedArrays = this.stats.measure(StatType.ADJUSTING_RANGES, () => {
const result = this.rangeMapping.moveAllRangesInSheetAfterAddingRows(addedRows.sheet, addedRows.rowStart, addedRows.numberOfRows);
this.fixRangesWhenAddingRows(addedRows.sheet, addedRows.rowStart, addedRows.numberOfRows);
return this.getArrayVerticesRelatedToRanges(result.verticesWithChangedSize);
});
this.stats.measure(StatType.ADJUSTING_ARRAY_MAPPING, () => {
this.fixArraysAfterAddingRow(addedRows.sheet, addedRows.rowStart, addedRows.numberOfRows);
});
for (const vertex of this.addressMapping.verticesFromRowsSpan(addedRows)) {
this.graph.markNodeAsDirty(vertex);
}
this.addStructuralNodesToChangeSet();
return {
affectedArrays
};
}
addColumns(addedColumns) {
this.stats.measure(StatType.ADJUSTING_ADDRESS_MAPPING, () => {
this.addressMapping.addColumns(addedColumns.sheet, addedColumns.columnStart, addedColumns.numberOfColumns);
});
const affectedArrays = this.stats.measure(StatType.ADJUSTING_RANGES, () => {
const result = this.rangeMapping.moveAllRangesInSheetAfterAddingColumns(addedColumns.sheet, addedColumns.columnStart, addedColumns.numberOfColumns);
this.fixRangesWhenAddingColumns(addedColumns.sheet, addedColumns.columnStart, addedColumns.numberOfColumns);
return this.getArrayVerticesRelatedToRanges(result.verticesWithChangedSize);
});
this.stats.measure(StatType.ADJUSTING_ARRAY_MAPPING, () => {
return this.fixArraysAfterAddingColumn(addedColumns.sheet, addedColumns.columnStart, addedColumns.numberOfColumns);
});
for (const vertex of this.addressMapping.verticesFromColumnsSpan(addedColumns)) {
this.graph.markNodeAsDirty(vertex);
}
this.addStructuralNodesToChangeSet();
return {
affectedArrays,
contentChanges: this.getAndClearContentChanges()
};
}
isThereSpaceForArray(arrayVertex) {
const range = arrayVertex.getRangeOrUndef();
if (range === undefined) {
return false;
}
for (const address of range.addresses(this)) {
const vertexUnderAddress = this.addressMapping.getCell(address);
if (vertexUnderAddress !== undefined && !(vertexUnderAddress instanceof EmptyCellVertex) && vertexUnderAddress !== arrayVertex) {
return false;
}
}
return true;
}
moveCells(sourceRange, toRight, toBottom, toSheet) {
for (const sourceAddress of sourceRange.addressesWithDirection(toRight, toBottom, this)) {
const targetAddress = simpleCellAddress(toSheet, sourceAddress.col + toRight, sourceAddress.row + toBottom);
let sourceVertex = this.addressMapping.getCell(sourceAddress);
const targetVertex = this.addressMapping.getCell(targetAddress);
this.addressMapping.removeCell(sourceAddress);
if (sourceVertex !== undefined) {
this.graph.markNodeAsDirty(sourceVertex);
this.addressMapping.setCell(targetAddress, sourceVertex);
let emptyVertex = undefined;
for (const adjacentNode of this.graph.adjacentNodes(sourceVertex)) {
if (adjacentNode instanceof RangeVertex && !sourceRange.containsRange(adjacentNode.range)) {
emptyVertex = emptyVertex !== null && emptyVertex !== void 0 ? emptyVertex : this.fetchCellOrCreateEmpty(sourceAddress).vertex;
this.graph.addEdge(emptyVertex, adjacentNode);
this.graph.removeEdge(sourceVertex, adjacentNode);
}
}
if (emptyVertex) {
this.graph.markNodeAsDirty(emptyVertex);
this.addressMapping.setCell(sourceAddress, emptyVertex);
}
}
if (targetVertex !== undefined) {
if (sourceVertex === undefined) {
this.addressMapping.removeCell(targetAddress);
}
for (const adjacentNode of this.graph.adjacentNodes(targetVertex)) {
sourceVertex = sourceVertex !== null && sourceVertex !== void 0 ? sourceVertex : this.fetchCellOrCreateEmpty(targetAddress).vertex;
this.graph.addEdge(sourceVertex, adjacentNode);
this.graph.markNodeAsDirty(sourceVertex);
}
this.removeVertex(targetVertex);
}
}
for (const rangeVertex of this.rangeMapping.rangeVerticesContainedInRange(sourceRange)) {
for (const adjacentNode of this.graph.adjacentNodes(rangeVertex)) {
if (adjacentNode instanceof RangeVertex && !sourceRange.containsRange(adjacentNode.range)) {
this.graph.removeEdge(rangeVertex, adjacentNode);
for (const address of rangeVertex.range.addresses(this)) {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(address);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, adjacentNode);
this.addressMapping.setCell(address, vertex);
this.graph.markNodeAsDirty(vertex);
}
}
}
}
this.rangeMapping.moveRangesInsideSourceRange(sourceRange, toRight, toBottom, toSheet);
}
/**
* Sets an array empty.
* - removes all corresponding entries from address mapping
* - reroutes the edges
* - removes vertex from graph and cleans up its dependencies
* - removes vertex from range mapping and array mapping
*/
setArrayEmpty(arrayVertex) {
const arrayRange = AbsoluteCellRange.spanFrom(arrayVertex.getAddress(this.lazilyTransformingAstService), arrayVertex.width, arrayVertex.height);
const dependentVertices = this.graph.adjacentNodes(arrayVertex);
for (const address of arrayRange.addresses(this)) {
this.addressMapping.removeCell(address);
}
for (const adjacentNode of dependentVertices.values()) {
const nodeDependencies = collectAddressesDependentToRange(this.functionRegistry, adjacentNode, arrayVertex.getRange(), this.lazilyTransformingAstService, this);
for (const address of nodeDependencies) {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(address);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, adjacentNode);
}
if (nodeDependencies.length > 0) {
this.graph.markNodeAsDirty(adjacentNode);
}
}
this.removeVertex(arrayVertex);
this.arrayMapping.removeArray(arrayVertex.getRange());
}
addVertex(address, vertex) {
this.graph.addNodeAndReturnId(vertex);
this.addressMapping.setCell(address, vertex);
}
addArrayVertex(address, vertex) {
this.graph.addNodeAndReturnId(vertex);
this.setAddressMappingForArrayVertex(vertex, address);
}
/**
* Iterator over all array formula nodes in the graph.
*/
*arrayFormulaNodes() {
for (const vertex of this.graph.getNodes()) {
if (vertex instanceof ArrayFormulaVertex) {
yield vertex;
}
}
}
*entriesFromRowsSpan(rowsSpan) {
yield* this.addressMapping.entriesFromRowsSpan(rowsSpan);
}
*entriesFromColumnsSpan(columnsSpan) {
yield* this.addressMapping.entriesFromColumnsSpan(columnsSpan);
}
fetchCell(address) {
return this.addressMapping.getCellOrThrow(address);
}
/**
* Gets the cell vertex at the specified address.
* @throws {NoSheetWithIdError} if sheet doesn't exist
*/
getCell(address) {
return this.addressMapping.getCell(address, {
throwIfSheetNotExists: true
});
}
getCellValue(address) {
if (this.isPlaceholder(address.sheet)) {
return new CellError(ErrorType.REF, ErrorMessage.SheetRef);
}
return this.addressMapping.getCellValue(address);
}
getRawValue(address) {
if (this.isPlaceholder(address.sheet)) {
return null;
}
return this.addressMapping.getRawValue(address);
}
getScalarValue(address) {
if (this.isPlaceholder(address.sheet)) {
return new CellError(ErrorType.REF, ErrorMessage.SheetRef);
}
const value = this.addressMapping.getCellValue(address);
if (value instanceof SimpleRangeValue) {
return new CellError(ErrorType.VALUE, ErrorMessage.ScalarExpected);
}
return value;
}
existsEdge(fromNode, toNode) {
return this.graph.existsEdge(fromNode, toNode);
}
getSheetId(sheetName) {
return this.sheetMapping.getSheetIdOrThrowError(sheetName);
}
getSheetHeight(sheet) {
return this.addressMapping.getSheetHeight(sheet);
}
getSheetWidth(sheet) {
return this.addressMapping.getSheetWidth(sheet);
}
getArray(range) {
return this.arrayMapping.getArray(range);
}
getRange(start, end) {
return this.rangeMapping.getRangeVertex(start, end);
}
topSortWithScc() {
return this.graph.topSortWithScc();
}
markAsVolatile(vertex) {
this.graph.markNodeAsVolatile(vertex);
}
markAsDependentOnStructureChange(vertex) {
this.graph.markNodeAsChangingWithStructure(vertex);
}
forceApplyPostponedTransformations() {
for (const vertex of this.graph.getNodes()) {
if (vertex instanceof ScalarFormulaVertex) {
vertex.ensureRecentData(this.lazilyTransformingAstService);
}
}
}
*rawValuesFromRange(range) {
for (const address of range.addresses(this)) {
const value = this.getScalarValue(address);
if (value !== EmptyValue) {
yield [getRawValue(value), address];
}
}
}
computeListOfValuesInRange(range) {
const values = [];
for (const cellFromRange of range.addresses(this)) {
const value = this.getScalarValue(cellFromRange);
values.push(value);
}
return values;
}
shrinkArrayToCorner(array) {
this.cleanAddressMappingUnderArray(array);
for (const adjacentVertex of this.adjacentArrayVertices(array)) {
let relevantDependencies;
if (adjacentVertex instanceof FormulaVertex) {
relevantDependencies = this.formulaDirectDependenciesToArray(adjacentVertex, array);
} else {
relevantDependencies = this.rangeDirectDependenciesToArray(adjacentVertex, array);
}
let dependentToCorner = false;
for (const [address, vertex] of relevantDependencies) {
if (array.isLeftCorner(address)) {
dependentToCorner = true;
}
this.graph.addEdge(vertex, adjacentVertex);
this.graph.markNodeAsDirty(vertex);
}
if (!dependentToCorner) {
this.graph.removeEdge(array, adjacentVertex);
}
}
this.graph.markNodeAsDirty(array);
}
isArrayInternalCell(address) {
const vertex = this.getCell(address);
return vertex instanceof ArrayFormulaVertex && !vertex.isLeftCorner(address);
}
getAndClearContentChanges() {
const changes = this.changes;
this.changes = ContentChanges.empty();
return changes;
}
getAdjacentNodesAddresses(inputVertex) {
const deps = this.graph.adjacentNodes(inputVertex);
const ret = [];
deps.forEach(vertex => {
if (vertex instanceof RangeVertex) {
ret.push(simpleCellRange(vertex.start, vertex.end));
} else if (vertex instanceof FormulaVertex) {
ret.push(vertex.getAddress(this.lazilyTransformingAstService));
}
});
return ret;
}
/**
* Marks all cell vertices in the sheet as dirty.
*/
markAllCellsAsDirtyInSheet(sheetId) {
const sheetCells = this.addressMapping.sheetEntries(sheetId);
for (const [, vertex] of sheetCells) {
this.graph.markNodeAsDirty(vertex);
}
}
/**
* Marks all range vertices in the sheet as dirty.
*/
markAllRangesAsDirtyInSheet(sheetId) {
const sheetRanges = this.rangeMapping.rangesInSheet(sheetId);
for (const vertex of sheetRanges) {
this.graph.markNodeAsDirty(vertex);
}
}
/**
* For each range vertex in placeholderSheetToDelete:
* - reroutes dependencies and dependents of range vertex to the corresponding vertex in sheetToKeep
* - removes range vertex from graph and range mapping
* - cleans up dependencies of the removed vertex
*/
mergeRangeVertices(sheetToKeep, placeholderSheetToDelete) {
const rangeVertices = Array.from(this.rangeMapping.rangesInSheet(placeholderSheetToDelete));
for (const vertexToDelete of rangeVertices) {
if (!this.graph.hasNode(vertexToDelete)) {
continue;
}
const start = vertexToDelete.start;
const end = vertexToDelete.end;
if (start.sheet !== placeholderSheetToDelete && end.sheet !== placeholderSheetToDelete) {
continue;
}
const targetStart = simpleCellAddress(sheetToKeep, start.col, start.row);
const targetEnd = simpleCellAddress(sheetToKeep, end.col, end.row);
const vertexToKeep = this.rangeMapping.getRangeVertex(targetStart, targetEnd);
if (vertexToKeep) {
this.rerouteDependents(vertexToDelete, vertexToKeep);
this.removeVertexAndRerouteDependencies(vertexToDelete, vertexToKeep);
this.rangeMapping.removeVertexIfExists(vertexToDelete);
this.graph.markNodeAsDirty(vertexToKeep);
} else {
this.rangeMapping.removeVertexIfExists(vertexToDelete);
vertexToDelete.range.moveToSheet(sheetToKeep);
this.rangeMapping.addOrUpdateVertex(vertexToDelete);
this.graph.markNodeAsDirty(vertexToDelete);
}
}
}
/**
* For each cell vertex in placeholderSheetToDelete:
* - reroutes dependents of cell vertex to the corresponding vertex in sheetToKeep
* - removes cell vertex from graph and address mapping
* - cleans up dependencies of the removed vertex
*/
mergeCellVertices(sheetToKeep, placeholderSheetToDelete) {
const cellVertices = Array.from(this.addressMapping.sheetEntries(placeholderSheetToDelete)); // placeholder sheet contains only EmptyCellVertex-es
for (const [addressToDelete, vertexToDelete] of cellVertices) {
const addressToKeep = simpleCellAddress(sheetToKeep, addressToDelete.col, addressToDelete.row);
const vertexToKeep = this.getCell(addressToKeep);
if (vertexToKeep) {
this.rerouteDependents(vertexToDelete, vertexToKeep);
this.removeVertexAndCleanupDependencies(vertexToDelete);
this.addressMapping.removeCell(addressToDelete);
this.graph.markNodeAsDirty(vertexToKeep);
} else {
this.addressMapping.moveCell(addressToDelete, addressToKeep);
this.graph.markNodeAsDirty(vertexToDelete);
}
}
}
/**
* Checks if the given sheet ID refers to a placeholder sheet (doesn't exist but is referenced by other sheets)
*/
isPlaceholder(sheetId) {
return sheetId !== NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS && !this.sheetMapping.hasSheetWithId(sheetId, {
includePlaceholders: false
});
}
exchangeGraphNode(oldNode, newNode) {
this.graph.addNodeAndReturnId(newNode);
const adjNodesStored = this.graph.adjacentNodes(oldNode);
this.removeVertex(oldNode);
adjNodesStored.forEach(adjacentNode => {
if (this.graph.hasNode(adjacentNode)) {
this.graph.addEdge(newNode, adjacentNode);
}
});
}
setArray(range, vertex) {
this.arrayMapping.setArray(range, vertex);
}
correctInfiniteRangesDependency(address) {
const relevantInfiniteRanges = this.graph.getInfiniteRanges().filter(({
node
}) => node.range.addressInRange(address));
if (relevantInfiniteRanges.length <= 0) {
return;
}
const {
vertex,
id: maybeVertexId
} = this.fetchCellOrCreateEmpty(address);
const vertexId = maybeVertexId !== null && maybeVertexId !== void 0 ? maybeVertexId : this.graph.getNodeId(vertex);
if (vertexId === undefined) {
throw new Error('Vertex not found');
}
relevantInfiniteRanges.forEach(({
id
}) => {
this.graph.addEdge(vertexId, id);
});
}
exchangeOrAddGraphNode(oldNode, newNode) {
if (oldNode) {
this.exchangeGraphNode(oldNode, newNode);
} else {
this.graph.addNodeAndReturnId(newNode);
}
}
getArrayVerticesRelatedToRanges(ranges) {
const arrayVertices = new Set();
ranges.forEach(range => {
if (!this.graph.hasNode(range)) {
return;
}
this.graph.adjacentNodes(range).forEach(adjacentVertex => {
if (adjacentVertex instanceof ArrayFormulaVertex) {
arrayVertices.add(adjacentVertex);
}
});
});
return arrayVertices;
}
correctInfiniteRangesDependenciesByRangeVertex(vertex) {
this.graph.getInfiniteRanges().forEach(({
id: infiniteRangeVertexId,
node: infiniteRangeVertex
}) => {
const intersection = vertex.range.intersectionWith(infiniteRangeVertex.range);
if (intersection === undefined) {
return;
}
intersection.addresses(this).forEach(address => {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(address);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, infiniteRangeVertexId);
});
});
}
cleanAddressMappingUnderArray(vertex) {
const arrayRange = vertex.getRange();
for (const address of arrayRange.addresses(this)) {
const oldValue = vertex.getArrayCellValue(address);
if (this.getCell(address) === vertex) {
if (vertex.isLeftCorner(address)) {
this.changes.addChange(new CellError(ErrorType.REF), address, oldValue);
} else {
this.addressMapping.removeCell(address);
this.changes.addChange(EmptyValue, address, oldValue);
}
} else {
this.changes.addChange(EmptyValue, address, oldValue);
}
}
}
*formulaDirectDependenciesToArray(vertex, array) {
var _a;
const [, formulaDependencies] = (_a = this.formulaDependencyQuery(vertex)) !== null && _a !== void 0 ? _a : [];
if (formulaDependencies === undefined) {
return;
}
for (const dependency of formulaDependencies) {
if (dependency instanceof NamedExpressionDependency || dependency instanceof AbsoluteCellRange) {
continue;
}
if (array.getRange().addressInRange(dependency)) {
const vertex = this.fetchCellOrCreateEmpty(dependency).vertex;
yield [dependency, vertex];
}
}
}
*rangeDirectDependenciesToArray(vertex, array) {
const {
restRange: range
} = this.rangeMapping.findSmallerRange(vertex.range);
for (const address of range.addresses(this)) {
if (array.getRange().addressInRange(address)) {
const cell = this.fetchCellOrCreateEmpty(address).vertex;
yield [address, cell];
}
}
}
*adjacentArrayVertices(vertex) {
const adjacentNodes = this.graph.adjacentNodes(vertex);
for (const item of adjacentNodes) {
if (item instanceof FormulaVertex || item instanceof RangeVertex) {
yield item;
}
}
}
addStructuralNodesToChangeSet() {
this.graph.markChangingWithStructureNodesAsDirty();
}
fixRangesWhenAddingRows(sheet, row, numberOfRows) {
const originalValues = Array.from(this.rangeMapping.rangesInSheet(sheet));
for (const rangeVertex of originalValues) {
if (rangeVertex.range.includesRow(row + numberOfRows)) {
if (rangeVertex.bruteForce) {
const addedSubrangeInThatRange = rangeVertex.range.rangeWithSameWidth(row, numberOfRows);
for (const address of addedSubrangeInThatRange.addresses(this)) {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(address);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, rangeVertex);
}
} else {
let currentRangeVertex = rangeVertex;
let find = this.rangeMapping.findSmallerRange(currentRangeVertex.range);
if (find.smallerRangeVertex !== undefined) {
continue;
}
while (find.smallerRangeVertex === undefined) {
const newRangeVertex = new RangeVertex(AbsoluteCellRange.spanFrom(currentRangeVertex.range.start, currentRangeVertex.range.width(), currentRangeVertex.range.height() - 1));
this.rangeMapping.addOrUpdateVertex(newRangeVertex);
this.graph.addNodeAndReturnId(newRangeVertex);
const restRange = new AbsoluteCellRange(simpleCellAddress(currentRangeVertex.range.start.sheet, currentRangeVertex.range.start.col, currentRangeVertex.range.end.row), currentRangeVertex.range.end);
this.addAllFromRange(restRange, currentRangeVertex);
this.graph.addEdge(newRangeVertex, currentRangeVertex);
currentRangeVertex = newRangeVertex;
find = this.rangeMapping.findSmallerRange(currentRangeVertex.range);
}
this.graph.addEdge(find.smallerRangeVertex, currentRangeVertex);
this.addAllFromRange(find.restRange, currentRangeVertex);
this.graph.removeEdge(find.smallerRangeVertex, rangeVertex);
}
}
}
}
addAllFromRange(range, rangeVertex) {
for (const address of range.addresses(this)) {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(address);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, rangeVertex);
}
}
fixRangesWhenAddingColumns(sheet, column, numberOfColumns) {
for (const rangeVertex of this.rangeMapping.rangesInSheet(sheet)) {
if (rangeVertex.range.includesColumn(column + numberOfColumns)) {
let subrange;
if (rangeVertex.bruteForce) {
subrange = rangeVertex.range.rangeWithSameHeight(column, numberOfColumns);
} else {
subrange = AbsoluteCellRange.spanFrom(simpleCellAddress(sheet, column, rangeVertex.range.end.row), numberOfColumns, 1);
}
for (const address of subrange.addresses(this)) {
const {
vertex,
id
} = this.fetchCellOrCreateEmpty(address);
this.graph.addEdge(id !== null && id !== void 0 ? id : vertex, rangeVertex);
}
}
}
}
exchangeOrAddFormulaVertex(vertex) {
const address = vertex.getAddress(this.lazilyTransformingAstService);
const range = AbsoluteCellRange.spanFrom(address, vertex.width, vertex.height);
const oldNode = this.shrinkPossibleArrayAndGetCell(address);
if (vertex instanceof ArrayFormulaVertex) {
this.setArray(range, vertex);
}
this.exchangeOrAddGraphNode(oldNode, vertex);
this.addressMapping.setCell(address, vertex);
if (vertex instanceof ArrayFormulaVertex) {
if (!this.isThereSpaceForArray(vertex)) {
return;
}
for (const cellAddress of range.addresses(this)) {
if (vertex.isLeftCorner(cellAddress)) {
continue;
}
const old = this.getCell(cellAddress);
this.exchangeOrAddGraphNode(old, vertex);
}
}
for (const cellAddress of range.addresses(this)) {
this.addressMapping.setCell(cellAddress, vertex);
}
}
setAddressMappingForArrayVertex(vertex, formulaAddress) {
this.addressMapping.setCell(formulaAddress, vertex);
if (!(vertex instanceof ArrayFormulaVertex)) {
return;
}
const range = AbsoluteCellRange.spanFromOrUndef(formulaAddress, vertex.width, vertex.height);
if (range === undefined) {
return;
}
this.setArray(range, vertex);
if (!this.isThereSpaceForArray(vertex)) {
return;
}
for (const address of range.addresses(this)) {
this.addressMapping.setCell(address, vertex);
}
}
truncateRanges(span, coordinate) {
const {
verticesToRemove,
verticesToMerge,
verticesWithChangedSize
} = this.rangeMapping.truncateRanges(span, coordinate);
for (const [existingVertex, mergedVertex] of verticesToMerge) {
this.rerouteDependents(mergedVertex, existingVertex);
this.removeVertexAndCleanupDependencies(mergedVertex);
}
for (const rangeVertex of verticesToRemove) {
this.removeVertexAndCleanupDependencies(rangeVertex);
}
return verticesWithChangedSize;
}
fixArraysAfterAddingRow(sheet, rowStart, numberOfRows) {
this.arrayMapping.moveArrayVerticesAfterRowByRows(sheet, rowStart, numberOfRows);
if (rowStart <= 0) {
return;
}
for (const [, array] of this.arrayMapping.arraysInRows(RowsSpan.fromRowStartAndEnd(sheet, rowStart - 1, rowStart - 1))) {
const arrayRange = array.getRange();
for (let col = arrayRange.start.col; col <= arrayRange.end.col; ++col) {
for (let row = rowStart; row <= arrayRange.end.row; ++row) {
const destination = simpleCellAddress(sheet, col, row);
const source = simpleCellAddress(sheet, col, row + numberOfRows);
const value = array.getArrayCellValue(destination);
this.addressMapping.moveCell(source, destination);
this.changes.addChange(EmptyValue, source, value);
}
}
}
}
fixArraysAfterRemovingRows(sheet, rowStart, numberOfRows) {
this.arrayMapping.moveArrayVerticesAfterRowByRows(sheet, rowStart, -numberOfRows);
if (rowStart <= 0) {
return;
}
for (const [, array] of this.arrayMapping.arraysInRows(RowsSpan.fromRowStartAndEnd(sheet, rowStart - 1, rowStart - 1))) {
if (this.isThereSpaceForArray(array)) {
for (const address of array.getRange().addresses(this)) {
this.addressMapping.setCell(address, array);
}
} else {
this.setNoSpaceIfArray(array);
}
}
}
fixArraysAfterAddingColumn(sheet, columnStart, numberOfColumns) {
this.arrayMapping.moveArrayVerticesAfterColumnByColumns(sheet, columnStart, numberOfColumns);
if (columnStart <= 0) {
return;
}
for (const [, array] of this.arrayMapping.arraysInCols(ColumnsSpan.fromColumnStartAndEnd(sheet, columnStart - 1, columnStart - 1))) {
const arrayRange = array.getRange();
for (let row = arrayRange.start.row; row <= arrayRange.end.row; ++row) {
for (let col = columnStart; col <= arrayRange.end.col; ++col) {
const destination = simpleCellAddress(sheet, col, row);
const source = simpleCellAddress(sheet, col + numberOfColumns, row);
const value = array.getArrayCellValue(destination);
this.addressMapping.moveCell(source, destination);
this.changes.addChange(EmptyValue, source, value);
}
}
}
}
fixArraysAfterRemovingColumns(sheet, columnStart, numberOfColumns) {
this.arrayMapping.moveArrayVerticesAfterColumnByColumns(sheet, columnStart, -numberOfColumns);
if (columnStart <= 0) {
return;
}
for (const [, array] of this.arrayMapping.arraysInCols(ColumnsSpan.fromColumnStartAndEnd(sheet, columnStart - 1, columnStart - 1))) {
if (this.isThereSpaceForArray(array)) {
for (const address of array.getRange().addresses(this)) {
this.addressMapping.setCell(address, array);
}
} else {
this.setNoSpaceIfArray(array);
}
}
}
shrinkPossibleArrayAndGetCell(address) {
const vertex = this.getCell(address);
if (!(vertex instanceof ArrayFormulaVertex)) {
return vertex;
}
this.setNoSpaceIfArray(vertex);
return this.getCell(address);
}
setNoSpaceIfArray(vertex) {
if (vertex instanceof ArrayFormulaVertex) {
this.shrinkArrayToCorner(vertex);
vertex.setNoSpace();
}
}
/**
* Removes a vertex from the graph and range mapping and cleans up its dependencies.
*/
removeVertex(vertex) {
this.removeVertexAndCleanupDependencies(vertex);
if (vertex instanceof RangeVertex) {
this.rangeMapping.removeVertexIfExists(vertex);
}
}
/**
* Reroutes dependent vertices of source to target. Also removes the edge target -> source if it exists.
*/
rerouteDependents(source, target) {
const dependents = this.graph.adjacentNodes(source);
this.graph.removeEdgeIfExists(target, source);
dependents.forEach(adjacentNode => {
if (this.graph.hasNode(adjacentNode)) {
this.graph.addEdge(target, adjacentNode);
}
});
}
/**
* Removes a vertex from graph and reroutes its dependencies to other vertex. Also removes the edge vertexToKeep -> vertexToDelete if it exists.
*/
removeVertexAndRerouteDependencies(vertexToDelete, vertexToKeep) {
const dependencies = this.graph.removeNode(vertexToDelete);
this.graph.removeEdgeIfExists(vertexToKeep, vertexToDelete);
dependencies.forEach(([_, dependency]) => {
if (this.graph.hasNode(dependency)) {
this.graph.addEdge(dependency, vertexToKeep);
}
});
}
/**
* Removes a vertex from graph and cleans up its dependencies.
* Dependency clean up = remove all RangeVertex and EmptyCellVertex dependencies if no other vertex depends on them.
* Also cleans up placeholder sheets that have no remaining vertices (not needed anymore)
*/
removeVertexAndCleanupDependencies(inputVertex) {
const dependencies = new Set(this.graph.removeNode(inputVertex));
const affectedSheets = new Set();
while (dependencies.size > 0) {
const dependency = dependencies.values().next().value;
dependencies.delete(dependency);
const [address, vertex] = dependency;
if (this.graph.hasNode(vertex) && this.graph.adjacentNodesCount(vertex) === 0) {
if (vertex instanceof RangeVertex || vertex instanceof EmptyCellVertex) {
this.graph.removeNode(vertex).forEach(candidate => dependencies.add(candidate));
}
if (vertex instanceof RangeVertex) {
this.rangeMapping.removeVertexIfExists(vertex);
affectedSheets.add(vertex.sheet);
} else if (vertex instanceof EmptyCellVertex && isSimpleCellAddress(address)) {
this.addressMapping.removeCell(address);
affectedSheets.add(address.sheet);
}
}
}
this.cleanupPlaceholderSheets(affectedSheets);
}
/**
* Removes placeholder sheets that have no remaining vertices.
*/
cleanupPlaceholderSheets(sheetIds) {
for (const sheetId of sheetIds) {
if (this.isPlaceholder(sheetId) && !this.addressMapping.hasAnyEntries(sheetId) && this.rangeMapping.getNumberOfRangesInSheet(sheetId) === 0) {
this.sheetMapping.removeSheetIfExists(sheetId, {
includePlaceholders: true
});
this.addressMapping.removeSheetIfExists(sheetId);
}
}
}
}