UNPKG

hyperformula

Version:

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

252 lines 8.11 kB
/** * @license * Copyright (c) 2025 Handsoncode. All rights reserved. */ import { AbsoluteCellRange } from "../AbsoluteCellRange.mjs"; import { simpleCellAddress } from "../Cell.mjs"; /** * Maintains a per-sheet map from serialized start/end coordinates to `RangeVertex`. * - Every range vertex in dependency graph should be stored in this mapping. * - Guarantees uniqueness: one vertex per distinct rectangle, enabling cache reuse. * - Implements "smaller prefix + tail row" optimization: if A1:A4 exists, A1:A5 depends on it + only A5. * - RangeVertex stores cached results for associative aggregates (SUM, COUNT) and criterion functions. */ export class RangeMapping { constructor() { /** * Map sheetId -> address of start and end (as string) -> vertex */ this.rangeMapping = new Map(); } /** * Returns number of ranges in the sheet or 0 if the sheet does not exist */ getNumberOfRangesInSheet(sheet) { var _a, _b; return (_b = (_a = this.rangeMapping.get(sheet)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0; } /** * Adds or updates vertex in the mapping */ addOrUpdateVertex(vertex) { let sheetMap = this.rangeMapping.get(vertex.sheet); if (sheetMap === undefined) { sheetMap = new Map(); this.rangeMapping.set(vertex.sheet, sheetMap); } const key = RangeMapping.calculateRangeKey(vertex.start, vertex.end); sheetMap.set(key, vertex); } /** * Removes vertex from the mapping if it exists */ removeVertexIfExists(vertex) { const sheet = vertex.sheet; const sheetMap = this.rangeMapping.get(sheet); if (sheetMap === undefined) { return; } const key = RangeMapping.calculateRangeKey(vertex.start, vertex.end); sheetMap.delete(key); if (sheetMap.size === 0) { this.rangeMapping.delete(sheet); } } /** * Returns associated vertex for given range */ getRangeVertex(start, end) { const sheetMap = this.rangeMapping.get(start.sheet); const key = RangeMapping.calculateRangeKey(start, end); return sheetMap === null || sheetMap === void 0 ? void 0 : sheetMap.get(key); } /** * Returns associated vertex for given range or throws an error if not found */ getVertexOrThrow(start, end) { const maybeRange = this.getRangeVertex(start, end); if (!maybeRange) { throw Error('Range does not exist'); } return maybeRange; } truncateRanges(span, coordinate) { const verticesToRemove = Array(); const updated = Array(); const verticesWithChangedSize = Array(); const sheet = span.sheet; for (const [key, vertex] of this.entriesFromSheet(span.sheet)) { const range = vertex.range; if (span.start <= coordinate(vertex.range.end)) { range.removeSpan(span); if (range.shouldBeRemoved()) { this.removeByKey(sheet, key); verticesToRemove.push(vertex); } else { updated.push([key, vertex]); } verticesWithChangedSize.push(vertex); } } const verticesToMerge = []; updated.sort((left, right) => RangeMapping.compareBy(left[1], right[1], coordinate)); for (const [oldKey, vertex] of updated) { const newKey = RangeMapping.calculateRangeKey(vertex.range.start, vertex.range.end); if (newKey === oldKey) { continue; } const existingVertex = this.getByKey(sheet, newKey); this.removeByKey(sheet, oldKey); if (existingVertex !== undefined && vertex != existingVertex) { verticesToMerge.push([existingVertex, vertex]); } else { this.addOrUpdateVertex(vertex); } } return { verticesToRemove, verticesToMerge, verticesWithChangedSize }; } moveAllRangesInSheetAfterAddingRows(sheet, row, numberOfRows) { return this.updateVerticesFromSheet(sheet, (key, vertex) => { if (row <= vertex.start.row) { vertex.range.shiftByRows(numberOfRows); return { changedSize: false, vertex: vertex }; } else if (row > vertex.start.row && row <= vertex.end.row) { vertex.range.expandByRows(numberOfRows); return { changedSize: true, vertex: vertex }; } else { return undefined; } }); } moveAllRangesInSheetAfterAddingColumns(sheet, column, numberOfColumns) { return this.updateVerticesFromSheet(sheet, (key, vertex) => { if (column <= vertex.start.col) { vertex.range.shiftByColumns(numberOfColumns); return { changedSize: false, vertex: vertex }; } else if (column > vertex.start.col && column <= vertex.end.col) { vertex.range.expandByColumns(numberOfColumns); return { changedSize: true, vertex: vertex }; } else { return undefined; } }); } moveRangesInsideSourceRange(sourceRange, toRight, toBottom, toSheet) { this.updateVerticesFromSheet(sourceRange.sheet, (key, vertex) => { if (sourceRange.containsRange(vertex.range)) { vertex.range.shiftByColumns(toRight); vertex.range.shiftByRows(toBottom); vertex.range.moveToSheet(toSheet); return { changedSize: false, vertex: vertex }; } else { return undefined; } }); } *rangesInSheet(sheet) { const sheetMap = this.rangeMapping.get(sheet); if (!sheetMap) { return; } yield* sheetMap.values(); } *rangeVerticesContainedInRange(sourceRange) { for (const rangeVertex of this.rangesInSheet(sourceRange.sheet)) { if (sourceRange.containsRange(rangeVertex.range)) { yield rangeVertex; } } } /** * Finds smaller range if exists. */ findSmallerRange(range) { if (range.height() > 1 && Number.isFinite(range.height())) { const valuesRangeEndRowLess = simpleCellAddress(range.end.sheet, range.end.col, range.end.row - 1); const rowLessVertex = this.getRangeVertex(range.start, valuesRangeEndRowLess); if (rowLessVertex !== undefined) { const restRange = AbsoluteCellRange.fromSimpleCellAddresses(simpleCellAddress(range.start.sheet, range.start.col, range.end.row), range.end); return { smallerRangeVertex: rowLessVertex, restRange }; } } return { restRange: range }; } /** * Calculates a string key from start and end addresses */ static calculateRangeKey(start, end) { return `${start.col},${start.row},${end.col},${end.row}`; } /** * Compares two range vertices by their start and end addresses using the provided coordinate function */ static compareBy(left, right, coordinate) { const leftStart = coordinate(left.range.start); const rightStart = coordinate(right.range.start); if (leftStart === rightStart) { const leftEnd = coordinate(left.range.end); const rightEnd = coordinate(right.range.end); return leftEnd - rightEnd; } else { return leftStart - rightStart; } } *entriesFromSheet(sheet) { const sheetMap = this.rangeMapping.get(sheet); if (!sheetMap) { return; } yield* sheetMap.entries(); } removeByKey(sheet, key) { const sheetMap = this.rangeMapping.get(sheet); if (!sheetMap) { throw new Error(`Sheet ${sheet} not found`); } sheetMap.delete(key); } getByKey(sheet, key) { var _a; return (_a = this.rangeMapping.get(sheet)) === null || _a === void 0 ? void 0 : _a.get(key); } updateVerticesFromSheet(sheet, fn) { const updated = Array(); for (const [key, vertex] of this.entriesFromSheet(sheet)) { const result = fn(key, vertex); if (result !== undefined) { this.removeByKey(sheet, key); updated.push(result); } } updated.forEach(entry => { this.addOrUpdateVertex(entry.vertex); }); return { verticesWithChangedSize: updated.filter(entry => entry.changedSize).map(entry => entry.vertex) }; } }