hyperformula
Version:
HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas
333 lines (331 loc) • 11.3 kB
JavaScript
"use strict";
exports.__esModule = true;
exports.SheetMapping = void 0;
var _errors = require("../errors");
var _i18n = require("../i18n");
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/
/**
* Representation of a sheet internal to SheetMapping. Not exported outside of this file.
*/
class Sheet {
constructor(id, displayName, isPlaceholder = false) {
this.id = id;
this.displayName = displayName;
this.isPlaceholder = isPlaceholder;
}
/**
* Returns the canonical (normalized) name of the sheet.
*/
get canonicalName() {
return SheetMapping.canonicalizeSheetName(this.displayName);
}
}
/**
* Manages the sheets in the instance.
* - Can convert between sheet names and ids and vice versa.
* - Also stores placeholders for sheets that are used in formulas but not yet added. They are marked as isPlaceholder=true.
* - Sheetnames thet differ only in case are considered the same. (See: canonicalizeSheetName)
*/
class SheetMapping {
constructor(languages) {
/**
* Last used sheet ID. Used to generate new sheet IDs.
*/
this.lastSheetId = -1;
/**
* Mapping from canonical sheet name to sheet ID.
*/
this.mappingFromCanonicalNameToId = new Map();
/**
* Mapping from sheet ID to sheet.
*/
this.allSheets = new Map();
this.sheetNamePrefix = languages.getUITranslation(_i18n.UIElement.NEW_SHEET_PREFIX);
}
/**
* Converts sheet name to canonical/normalized form.
* @static
*/
static canonicalizeSheetName(sheetDisplayName) {
return sheetDisplayName.toLowerCase();
}
/**
* Returns sheet ID for the given name. By default excludes placeholders.
*/
getSheetId(sheetName, options = {}) {
var _a;
return (_a = this._getSheetByName(sheetName, options)) === null || _a === void 0 ? void 0 : _a.id;
}
/**
* Returns sheet ID for the given name. Excludes placeholders.
*
* @throws {NoSheetWithNameError} if the sheet with the given name does not exist.
*/
getSheetIdOrThrowError(sheetName) {
const sheet = this._getSheetByName(sheetName, {});
if (sheet === undefined) {
throw new _errors.NoSheetWithNameError(sheetName);
}
return sheet.id;
}
/**
* Returns display name for the given sheet ID. Excludes placeholders.
*
* @returns {Maybe<string>} the display name, or undefined if the sheet with the given ID does not exist.
*/
getSheetName(sheetId) {
var _a;
return (_a = this._getSheet(sheetId, {})) === null || _a === void 0 ? void 0 : _a.displayName;
}
/**
* Returns display name for the given sheet ID. Excludes placeholders.
*
* @throws {NoSheetWithIdError} if the sheet with the given ID does not exist.
*/
getSheetNameOrThrowError(sheetId, options = {}) {
return this._getSheetOrThrowError(sheetId, options).displayName;
}
/**
* Iterates over all sheet display names. By default excludes placeholders.
*/
*iterateSheetNames(options = {}) {
for (const sheet of this.allSheets.values()) {
if (options.includePlaceholders || !sheet.isPlaceholder) {
yield sheet.displayName;
}
}
}
/**
* Returns array of all sheet display names. By default excludes placeholders.
*/
getSheetNames(options = {}) {
return Array.from(this.iterateSheetNames(options));
}
/**
* Returns total count of sheets. By default excludes placeholders.
*/
numberOfSheets(options = {}) {
return this.getSheetNames(options).length;
}
/**
* Checks if sheet with given ID exists. By default excludes placeholders.
*/
hasSheetWithId(sheetId, options = {}) {
return this._getSheet(sheetId, options) !== undefined;
}
/**
* Checks if sheet with given name exists (case-insensitive). Excludes placeholders.
*/
hasSheetWithName(sheetName) {
return this._getSheetByName(sheetName, {}) !== undefined;
}
/**
* Adds new sheet with optional name and returns its ID.
* If called with a name of an existing placeholder sheet, converts the placeholder sheet to a real sheet.
*
* @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists.
*/
addSheet(newSheetDisplayName = `${this.sheetNamePrefix}${this.lastSheetId + 2}`) {
const sheetWithConflictingName = this._getSheetByName(newSheetDisplayName, {
includePlaceholders: true
});
if (sheetWithConflictingName) {
if (!sheetWithConflictingName.isPlaceholder) {
throw new _errors.SheetNameAlreadyTakenError(newSheetDisplayName);
}
sheetWithConflictingName.isPlaceholder = false;
return sheetWithConflictingName.id;
}
this.lastSheetId++;
const sheet = new Sheet(this.lastSheetId, newSheetDisplayName);
this._storeSheetInMappings(sheet);
return sheet.id;
}
/**
* Adds a sheet with a specific ID and name. Used for redo operations.
* If called with a name of an existing placeholder sheet, converts the placeholder sheet to a real sheet.
*
* @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists.
*/
addSheetWithId(sheetId, sheetDisplayName) {
const sheetWithConflictingName = this._getSheetByName(sheetDisplayName, {
includePlaceholders: true
});
if (sheetWithConflictingName) {
if (sheetWithConflictingName.id !== sheetId) {
throw new _errors.SheetNameAlreadyTakenError(sheetDisplayName);
}
if (!sheetWithConflictingName.isPlaceholder) {
throw new _errors.SheetNameAlreadyTakenError(sheetDisplayName);
}
sheetWithConflictingName.isPlaceholder = false;
return;
}
if (sheetId > this.lastSheetId) {
this.lastSheetId = sheetId;
}
const sheet = new Sheet(sheetId, sheetDisplayName);
this._storeSheetInMappings(sheet);
}
/**
* Adds a placeholder sheet with the given name if it does not exist yet
*/
addPlaceholderIfNotExists(sheetName) {
const sheetWithConflictingName = this._getSheetByName(sheetName, {
includePlaceholders: true
});
if (sheetWithConflictingName) {
return sheetWithConflictingName.id;
}
this.lastSheetId++;
const sheet = new Sheet(this.lastSheetId, sheetName, true);
this._storeSheetInMappings(sheet);
return sheet.id;
}
/**
* Adds a placeholder sheet with a specific ID and name.
* Used for undo operations to restore previously merged placeholder sheets.
*
* @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists.
*/
addPlaceholderWithId(sheetId, sheetDisplayName) {
const sheetWithConflictingName = this._getSheetByName(sheetDisplayName, {
includePlaceholders: true
});
if (sheetWithConflictingName) {
throw new _errors.SheetNameAlreadyTakenError(sheetDisplayName);
}
if (this.hasSheetWithId(sheetId, {
includePlaceholders: true
})) {
throw new Error(`Sheet with id ${sheetId} already exists`);
}
if (sheetId > this.lastSheetId) {
this.lastSheetId = sheetId;
}
const sheet = new Sheet(sheetId, sheetDisplayName, true);
this._storeSheetInMappings(sheet);
}
/**
*
* Removes sheet with given ID.
* If sheet does not exist, does nothing.
* @returns {boolean} true if sheet was removed, false if it did not exist.
*/
removeSheetIfExists(sheetId, options = {}) {
const sheet = this._getSheet(sheetId, options);
if (!sheet) {
return false;
}
this.allSheets.delete(sheetId);
this.mappingFromCanonicalNameToId.delete(sheet.canonicalName);
if (sheetId === this.lastSheetId) {
this.lastSheetId--;
}
return true;
}
/**
* Marks sheet with given ID as a placeholder.
* @throws {NoSheetWithIdError} if the sheet with the given ID does not exist
*/
markSheetAsPlaceholder(sheetId) {
const sheet = this._getSheetOrThrowError(sheetId, {});
sheet.isPlaceholder = true;
}
/**
* Renames sheet.
* - If called with sheetId of a placeholder sheet, throws {NoSheetWithIdError}.
* - If newDisplayName is conflicting with an existing sheet, throws {SheetNameAlreadyTakenError}.
* - If newDisplayName is conflicting with a placeholder sheet name, deletes the placeholder sheet and returns its id as mergedWithPlaceholderSheet.
*
* @throws {SheetNameAlreadyTakenError} if the sheet with the given name already exists.
* @throws {NoSheetWithIdError} if the sheet with the given ID does not exist.
*/
renameSheet(sheetId, newDisplayName) {
const sheet = this._getSheetOrThrowError(sheetId, {});
const currentDisplayName = sheet.displayName;
if (currentDisplayName === newDisplayName) {
return {
previousDisplayName: undefined
};
}
const sheetWithConflictingName = this._getSheetByName(newDisplayName, {
includePlaceholders: true
});
let mergedWithPlaceholderSheet = undefined;
if (sheetWithConflictingName !== undefined && sheetWithConflictingName.id !== sheet.id) {
if (!sheetWithConflictingName.isPlaceholder) {
throw new _errors.SheetNameAlreadyTakenError(newDisplayName);
} else {
this.mappingFromCanonicalNameToId.delete(sheetWithConflictingName.canonicalName);
this.allSheets.delete(sheetWithConflictingName.id);
if (sheetWithConflictingName.id === this.lastSheetId) {
this.lastSheetId--;
}
mergedWithPlaceholderSheet = sheetWithConflictingName.id;
}
}
const currentCanonicalName = sheet.canonicalName;
this.mappingFromCanonicalNameToId.delete(currentCanonicalName);
sheet.displayName = newDisplayName;
this._storeSheetInMappings(sheet);
return {
previousDisplayName: currentDisplayName,
mergedWithPlaceholderSheet
};
}
/**
* Stores sheet in both internal mappings.
* - If ID exists, it is updated. If not, it is added.
* - If canonical name exists, it is updated. If not, it is added.
*
* @internal
*/
_storeSheetInMappings(sheet) {
this.allSheets.set(sheet.id, sheet);
this.mappingFromCanonicalNameToId.set(sheet.canonicalName, sheet.id);
}
/**
* Returns sheet by ID
*
* @returns {Maybe<Sheet>} the sheet, or undefined if not found.
* @internal
*/
_getSheet(sheetId, options) {
const retrievedSheet = this.allSheets.get(sheetId);
if (retrievedSheet === undefined) {
return undefined;
}
return options.includePlaceholders || !retrievedSheet.isPlaceholder ? retrievedSheet : undefined;
}
/**
* Returns sheet by name
*
* @returns {Maybe<Sheet>} the sheet, or undefined if not found.
* @internal
*/
_getSheetByName(sheetName, options) {
const sheetId = this.mappingFromCanonicalNameToId.get(SheetMapping.canonicalizeSheetName(sheetName));
if (sheetId === undefined) {
return undefined;
}
return this._getSheet(sheetId, options);
}
/**
* Returns sheet by ID
*
* @throws {NoSheetWithIdError} if the sheet with the given ID does not exist.
* @internal
*/
_getSheetOrThrowError(sheetId, options) {
const sheet = this._getSheet(sheetId, options);
if (sheet === undefined) {
throw new _errors.NoSheetWithIdError(sheetId);
}
return sheet;
}
}
exports.SheetMapping = SheetMapping;