hyperformula
Version:
HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas
331 lines (329 loc) • 11.2 kB
JavaScript
"use strict";
exports.__esModule = true;
exports.AddressMapping = void 0;
var _errors = require("../../errors");
var _InterpreterValue = require("../../interpreter/InterpreterValue");
var _index = require("../index");
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/
/**
* Manages cell vertices and provides access to vertex by SimpleCellAddress.
* For each sheet it stores vertices according to AddressMappingStrategy: DenseStrategy or SparseStrategy.
*
* Pleceholder sheets:
* - for placeholders sheets (sheets that are used in formulas but not yet added), it stores placeholder strategy entries (DenseStrategy(0, 0))
* - placeholder strategy entries may contain EmptyCellVertex-es but never ValueCellVertex or FormulaVertex as they content is empty
* - vertices in placeholder strategy entries are used only for dependency tracking
*/
class AddressMapping {
constructor(policy) {
this.policy = policy;
this.mapping = new Map();
}
/**
* Gets the cell vertex at the specified address.
*/
getCell(address, options = {}) {
const sheetMapping = this.mapping.get(address.sheet);
if (!sheetMapping) {
if (options.throwIfSheetNotExists) {
throw new _errors.NoSheetWithIdError(address.sheet);
}
return undefined;
}
const cell = sheetMapping.getCell(address);
if (!cell && options.throwIfCellNotExists) {
throw Error('Vertex for address missing in AddressMapping');
}
return cell;
}
/**
* Gets the cell vertex at the specified address or throws if it doesn't exist.
* @throws {NoSheetWithIdError} if sheet doesn't exist
* @throws {Error} if cell doesn't exist
*/
getCellOrThrow(address) {
const sheetMapping = this.mapping.get(address.sheet);
if (!sheetMapping) {
throw new _errors.NoSheetWithIdError(address.sheet);
}
const cell = sheetMapping.getCell(address);
if (!cell) {
throw Error('Vertex for address missing in AddressMapping');
}
return cell;
}
/**
* Gets the address mapping strategy for the specified sheet.
* @throws {NoSheetWithIdError} if sheet doesn't exist
*/
getStrategyForSheetOrThrow(sheetId) {
const strategy = this.mapping.get(sheetId);
if (strategy === undefined) {
throw new _errors.NoSheetWithIdError(sheetId);
}
return strategy;
}
/**
* Adds a new sheet with the specified strategy.
* @throws {Error} if sheet is already added and throwIfSheetAlreadyExists is true
*/
addSheetWithStrategy(sheetId, strategy, options = {
throwIfSheetAlreadyExists: true
}) {
const strategyFound = this.mapping.get(sheetId);
if (strategyFound) {
if (options.throwIfSheetAlreadyExists) {
throw Error('Sheet already added');
}
return strategyFound;
}
this.mapping.set(sheetId, strategy);
return strategy;
}
/**
* Adds a sheet or changes the strategy for an existing sheet.
* Designed for the purpose of exchanging the placeholder strategy for a real strategy.
*/
addSheetOrChangeStrategy(sheetId, sheetBoundaries) {
const newStrategy = this.createStrategyBasedOnBoundaries(sheetBoundaries);
const strategyPlaceholder = this.mapping.get(sheetId);
if (!strategyPlaceholder) {
this.mapping.set(sheetId, newStrategy);
return newStrategy;
}
if (newStrategy instanceof _index.DenseStrategy) {
// new strategy is the same as the placeholder
return strategyPlaceholder;
}
this.moveStrategyContent(strategyPlaceholder, newStrategy, sheetId);
this.mapping.set(sheetId, newStrategy);
return newStrategy;
}
/**
* Moves the content of the source strategy to the target strategy.
*/
moveStrategyContent(sourceStrategy, targetStrategy, sheetContext) {
const sourceVertices = sourceStrategy.getEntries(sheetContext);
for (const [address, vertex] of sourceVertices) {
targetStrategy.setCell(address, vertex);
}
}
/**
* Adds a sheet and sets the strategy based on the sheet boundaries.
* @throws {Error} if sheet already exists and throwIfSheetAlreadyExists is true
*/
addSheetAndSetStrategyBasedOnBoundaries(sheetId, sheetBoundaries, options = {
throwIfSheetAlreadyExists: true
}) {
this.addSheetWithStrategy(sheetId, this.createStrategyBasedOnBoundaries(sheetBoundaries), options);
}
/**
* Creates a strategy based on the sheet boundaries.
*/
createStrategyBasedOnBoundaries(sheetBoundaries) {
const {
height,
width,
fill
} = sheetBoundaries;
const strategyConstructor = this.policy.call(fill);
return new strategyConstructor(width, height);
}
/**
* Adds a placeholder strategy (DenseStrategy) for a sheet. If the sheet already exists, does nothing.
*/
addSheetStrategyPlaceholderIfNotExists(sheetId) {
if (this.mapping.has(sheetId)) {
return;
}
this.mapping.set(sheetId, new _index.DenseStrategy(0, 0));
}
/**
* Removes a sheet from the address mapping.
* If sheet does not exist, does nothing.
* @returns {boolean} true if sheet was removed, false if it did not exist.
*/
removeSheetIfExists(sheetId) {
return this.mapping.delete(sheetId);
}
/**
* Gets the interpreter value of a cell at the specified address.
* @returns {InterpreterValue} The interpreter value (returns EmptyValue if cell doesn't exist)
*/
getCellValue(address) {
const vertex = this.getCell(address);
if (vertex === undefined) {
return _InterpreterValue.EmptyValue;
} else if (vertex instanceof _index.ArrayFormulaVertex) {
return vertex.getArrayCellValue(address);
} else {
return vertex.getCellValue();
}
}
/**
* Gets the raw cell content at the specified address.
* @returns {RawCellContent} The raw cell content or null if cell doesn't exist or is not a value cell
*/
getRawValue(address) {
const vertex = this.getCell(address);
if (vertex instanceof _index.ValueCellVertex) {
return vertex.getValues().rawValue;
} else if (vertex instanceof _index.ArrayFormulaVertex) {
return vertex.getArrayCellRawValue(address);
} else {
return null;
}
}
/**
* Sets a cell vertex at the specified address.
* @throws {Error} if sheet not initialized
*/
setCell(address, newVertex) {
const sheetMapping = this.mapping.get(address.sheet);
if (!sheetMapping) {
throw Error('Sheet not initialized');
}
sheetMapping.setCell(address, newVertex);
}
/**
* Moves a cell from source address to destination address.
* Supports cross-sheet moves (used for placeholder sheet merging).
* @throws {Error} if source sheet not initialized
* @throws {Error} if destination occupied
* @throws {Error} if source cell doesn't exist
*/
moveCell(source, destination) {
const sheetMapping = this.mapping.get(source.sheet);
if (!sheetMapping) {
throw Error('Sheet not initialized.');
}
if (this.has(destination)) {
throw new Error('Cannot move cell. Destination already occupied.');
}
const vertex = sheetMapping.getCell(source);
if (vertex === undefined) {
throw new Error('Cannot move cell. No cell with such address.');
}
this.setCell(destination, vertex);
this.removeCell(source);
}
/**
* Removes a cell at the specified address.
* @throws Error if sheet not initialized
*/
removeCell(address) {
const sheetMapping = this.mapping.get(address.sheet);
if (!sheetMapping) {
throw Error('Sheet not initialized');
}
sheetMapping.removeCell(address);
}
/**
* Checks if a cell exists at the specified address.
*/
has(address) {
const sheetMapping = this.mapping.get(address.sheet);
if (sheetMapping === undefined) {
return false;
}
return sheetMapping.has(address);
}
/**
* Gets the height of the specified sheet.
*/
getSheetHeight(sheetId) {
const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
return sheetMapping.getHeight();
}
/**
* Gets the width of the specified sheet.
*/
getSheetWidth(sheetId) {
const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
return sheetMapping.getWidth();
}
/**
* Adds rows to a sheet.
*/
addRows(sheetId, row, numberOfRows) {
const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
sheetMapping.addRows(row, numberOfRows);
}
/**
* Removes rows from a sheet.
*/
removeRows(removedRows) {
const sheetMapping = this.getStrategyForSheetOrThrow(removedRows.sheet);
sheetMapping.removeRows(removedRows);
}
/**
* Adds columns to a sheet starting at the specified column index.
*/
addColumns(sheetId, column, numberOfColumns) {
const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
sheetMapping.addColumns(column, numberOfColumns);
}
/**
* Removes columns from a sheet.
*/
removeColumns(removedColumns) {
const sheetMapping = this.getStrategyForSheetOrThrow(removedColumns.sheet);
sheetMapping.removeColumns(removedColumns);
}
/**
* Returns an iterator of cell vertices within the specified rows span.
*/
*verticesFromRowsSpan(rowsSpan) {
yield* this.mapping.get(rowsSpan.sheet).verticesFromRowsSpan(rowsSpan); // eslint-disable-line @typescript-eslint/no-non-null-assertion
}
/**
* Returns an iterator of cell vertices within the specified columns span.
*/
*verticesFromColumnsSpan(columnsSpan) {
yield* this.mapping.get(columnsSpan.sheet).verticesFromColumnsSpan(columnsSpan); // eslint-disable-line @typescript-eslint/no-non-null-assertion
}
/**
* Returns an iterator of address-vertex pairs within the specified rows span.
*/
*entriesFromRowsSpan(rowsSpan) {
const sheetMapping = this.getStrategyForSheetOrThrow(rowsSpan.sheet);
yield* sheetMapping.entriesFromRowsSpan(rowsSpan);
}
/**
* Returns an iterator of address-vertex pairs within the specified columns span.
*/
*entriesFromColumnsSpan(columnsSpan) {
const sheetMapping = this.getStrategyForSheetOrThrow(columnsSpan.sheet);
yield* sheetMapping.entriesFromColumnsSpan(columnsSpan);
}
/**
* Returns an iterator of all address-vertex pairs across all sheets.
* @returns {IterableIterator<[SimpleCellAddress, Maybe<CellVertex>]>} Iterator of [address, vertex] tuples
*/
*entries() {
for (const [sheet, mapping] of this.mapping.entries()) {
yield* mapping.getEntries(sheet);
}
}
/**
* Returns an iterator of address-vertex pairs for a specific sheet.
* @returns {IterableIterator<[SimpleCellAddress, CellVertex]>} Iterator of [address, vertex] tuples
* @throws {NoSheetWithIdError} if sheet doesn't exist
*/
*sheetEntries(sheetId) {
const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
yield* sheetMapping.getEntries(sheetId);
}
/**
* Checks if a sheet has any entries.
* @throws {NoSheetWithIdError} if sheet doesn't exist
*/
hasAnyEntries(sheetId) {
const iterator = this.sheetEntries(sheetId);
return !iterator.next().done;
}
}
exports.AddressMapping = AddressMapping;