hyperformula
Version:
HyperFormula is a JavaScript engine for efficient processing of spreadsheet-like data and formulas
298 lines • 11.1 kB
JavaScript
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/
import { simpleCellAddress } from "./Cell.mjs";
import { AstNodeType } from "./parser/index.mjs";
import { CELL_REFERENCE_PATTERN, NAMED_EXPRESSION_PATTERN, R1C1_CELL_REFERENCE_PATTERN } from "./parser/parser-consts.mjs";
export class InternalNamedExpression {
constructor(displayName, address, added, options) {
this.displayName = displayName;
this.address = address;
this.added = added;
this.options = options;
}
normalizeExpressionName() {
return this.displayName.toLowerCase();
}
copy() {
return new InternalNamedExpression(this.displayName, this.address, this.added, this.options);
}
}
class WorkbookStore {
constructor() {
this.mapping = new Map();
}
has(expressionName) {
return this.mapping.has(this.normalizeExpressionName(expressionName));
}
isNameAvailable(expressionName) {
const normalizedExpressionName = this.normalizeExpressionName(expressionName);
const namedExpression = this.mapping.get(normalizedExpressionName);
return !(namedExpression && namedExpression.added);
}
add(namedExpression) {
this.mapping.set(namedExpression.normalizeExpressionName(), namedExpression);
}
get(expressionName) {
return this.mapping.get(this.normalizeExpressionName(expressionName));
}
getExisting(expressionName) {
const namedExpression = this.mapping.get(this.normalizeExpressionName(expressionName));
if (namedExpression && namedExpression.added) {
return namedExpression;
} else {
return undefined;
}
}
remove(expressionName) {
const normalizedExpressionName = this.normalizeExpressionName(expressionName);
const namedExpression = this.mapping.get(normalizedExpressionName);
if (namedExpression) {
namedExpression.added = false;
}
}
getAllNamedExpressions() {
return Array.from(this.mapping.values()).filter(ne => ne.added);
}
normalizeExpressionName(expressionName) {
return expressionName.toLowerCase();
}
}
class WorksheetStore {
constructor() {
this.mapping = new Map();
}
add(namedExpression) {
this.mapping.set(this.normalizeExpressionName(namedExpression.displayName), namedExpression);
}
get(expressionName) {
return this.mapping.get(this.normalizeExpressionName(expressionName));
}
has(expressionName) {
return this.mapping.has(this.normalizeExpressionName(expressionName));
}
getAllNamedExpressions() {
return Array.from(this.mapping.values()).filter(ne => ne.added);
}
isNameAvailable(expressionName) {
const normalizedExpressionName = this.normalizeExpressionName(expressionName);
return !this.mapping.has(normalizedExpressionName);
}
remove(expressionName) {
const normalizedExpressionName = this.normalizeExpressionName(expressionName);
const namedExpression = this.mapping.get(normalizedExpressionName);
if (namedExpression) {
this.mapping.delete(normalizedExpressionName);
}
}
normalizeExpressionName(expressionName) {
return expressionName.toLowerCase();
}
}
export class NamedExpressions {
constructor() {
this.nextNamedExpressionRow = 0;
this.workbookStore = new WorkbookStore();
this.worksheetStores = new Map();
this.addressCache = new Map();
}
isNameAvailable(expressionName, sheetId) {
var _a, _b;
if (sheetId === undefined) {
return this.workbookStore.isNameAvailable(expressionName);
} else {
return (_b = (_a = this.worksheetStore(sheetId)) === null || _a === void 0 ? void 0 : _a.isNameAvailable(expressionName)) !== null && _b !== void 0 ? _b : true;
}
}
namedExpressionInAddress(row) {
const namedExpression = this.addressCache.get(row);
if (namedExpression && namedExpression.added) {
return namedExpression;
} else {
return undefined;
}
}
namedExpressionForScope(expressionName, sheetId) {
var _a;
if (sheetId === undefined) {
return this.workbookStore.getExisting(expressionName);
} else {
return (_a = this.worksheetStore(sheetId)) === null || _a === void 0 ? void 0 : _a.get(expressionName);
}
}
nearestNamedExpression(expressionName, sheetId) {
var _a, _b;
return (_b = (_a = this.worksheetStore(sheetId)) === null || _a === void 0 ? void 0 : _a.get(expressionName)) !== null && _b !== void 0 ? _b : this.workbookStore.getExisting(expressionName);
}
isExpressionInScope(expressionName, sheetId) {
var _a, _b;
return (_b = (_a = this.worksheetStore(sheetId)) === null || _a === void 0 ? void 0 : _a.has(expressionName)) !== null && _b !== void 0 ? _b : false;
}
/**
* Checks the validity of a named-expression's name.
*
* The name:
* - Must start with a Unicode letter or with an underscore (`_`).
* - Can contain only Unicode letters, numbers, underscores, and periods (`.`).
* - Can't be the same as any possible reference in the A1 notation (`[A-Za-z]+[0-9]+`).
* - Can't be the same as any possible reference in the R1C1 notation (`[rR][0-9]*[cC][0-9]*`).
*
* The naming rules follow the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html#__RefHeading__1017964_715980110) standard.
*/
isNameValid(expressionName) {
const a1CellRefRegexp = new RegExp(`^${CELL_REFERENCE_PATTERN}$`);
const r1c1CellRefRegexp = new RegExp(`^${R1C1_CELL_REFERENCE_PATTERN}$`);
const namedExpRegexp = new RegExp(`^${NAMED_EXPRESSION_PATTERN}$`);
if (a1CellRefRegexp.test(expressionName) || r1c1CellRefRegexp.test(expressionName)) {
return false;
}
return namedExpRegexp.test(expressionName);
}
addNamedExpression(expressionName, sheetId, options) {
const store = sheetId === undefined ? this.workbookStore : this.worksheetStoreOrCreate(sheetId);
let namedExpression = store.get(expressionName);
if (namedExpression !== undefined) {
namedExpression.added = true;
namedExpression.displayName = expressionName;
namedExpression.options = options;
} else {
namedExpression = new InternalNamedExpression(expressionName, this.nextAddress(), true, options);
store.add(namedExpression);
}
this.addressCache.set(namedExpression.address.row, namedExpression);
return namedExpression;
}
restoreNamedExpression(namedExpression, sheetId) {
const store = sheetId === undefined ? this.workbookStore : this.worksheetStoreOrCreate(sheetId);
namedExpression.added = true;
store.add(namedExpression);
this.addressCache.set(namedExpression.address.row, namedExpression);
return namedExpression;
}
namedExpressionOrPlaceholder(expressionName, sheetId) {
var _a;
return (_a = this.worksheetStoreOrCreate(sheetId).get(expressionName)) !== null && _a !== void 0 ? _a : this.workbookNamedExpressionOrPlaceholder(expressionName);
}
workbookNamedExpressionOrPlaceholder(expressionName) {
let namedExpression = this.workbookStore.get(expressionName);
if (namedExpression === undefined) {
namedExpression = new InternalNamedExpression(expressionName, this.nextAddress(), false);
this.workbookStore.add(namedExpression);
}
return namedExpression;
}
remove(expressionName, sheetId) {
let store;
if (sheetId === undefined) {
store = this.workbookStore;
} else {
store = this.worksheetStore(sheetId);
}
const namedExpression = store === null || store === void 0 ? void 0 : store.get(expressionName);
if (store === undefined || namedExpression === undefined || !namedExpression.added) {
throw Error('Named expression does not exist');
}
store.remove(expressionName);
if (store instanceof WorksheetStore && store.mapping.size === 0) {
this.worksheetStores.delete(sheetId);
}
this.addressCache.delete(namedExpression.address.row);
}
getAllNamedExpressionsNamesInScope(sheetId) {
return this.getAllNamedExpressions().filter(({
scope
}) => scope === sheetId).map(ne => ne.expression.displayName);
}
getAllNamedExpressionsNames() {
return this.getAllNamedExpressions().map(ne => ne.expression.displayName);
}
getAllNamedExpressions() {
const storedNamedExpressions = [];
this.workbookStore.getAllNamedExpressions().forEach(expr => {
storedNamedExpressions.push({
expression: expr,
scope: undefined
});
});
this.worksheetStores.forEach((store, sheetNum) => {
store.getAllNamedExpressions().forEach(expr => {
storedNamedExpressions.push({
expression: expr,
scope: sheetNum
});
});
});
return storedNamedExpressions;
}
getAllNamedExpressionsForScope(scope) {
var _a, _b;
if (scope === undefined) {
return this.workbookStore.getAllNamedExpressions();
} else {
return (_b = (_a = this.worksheetStores.get(scope)) === null || _a === void 0 ? void 0 : _a.getAllNamedExpressions()) !== null && _b !== void 0 ? _b : [];
}
}
worksheetStoreOrCreate(sheetId) {
let store = this.worksheetStores.get(sheetId);
if (!store) {
store = new WorksheetStore();
this.worksheetStores.set(sheetId, store);
}
return store;
}
worksheetStore(sheetId) {
return this.worksheetStores.get(sheetId);
}
nextAddress() {
return simpleCellAddress(NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS, 0, this.nextNamedExpressionRow++);
}
}
NamedExpressions.SHEET_FOR_WORKBOOK_EXPRESSIONS = -1;
export const doesContainRelativeReferences = ast => {
switch (ast.type) {
case AstNodeType.EMPTY:
case AstNodeType.NUMBER:
case AstNodeType.STRING:
case AstNodeType.ERROR:
case AstNodeType.ERROR_WITH_RAW_INPUT:
return false;
case AstNodeType.CELL_REFERENCE:
return !ast.reference.isAbsolute();
case AstNodeType.CELL_RANGE:
case AstNodeType.COLUMN_RANGE:
case AstNodeType.ROW_RANGE:
return !ast.start.isAbsolute();
case AstNodeType.NAMED_EXPRESSION:
return false;
case AstNodeType.PERCENT_OP:
case AstNodeType.PLUS_UNARY_OP:
case AstNodeType.MINUS_UNARY_OP:
{
return doesContainRelativeReferences(ast.value);
}
case AstNodeType.CONCATENATE_OP:
case AstNodeType.EQUALS_OP:
case AstNodeType.NOT_EQUAL_OP:
case AstNodeType.LESS_THAN_OP:
case AstNodeType.GREATER_THAN_OP:
case AstNodeType.LESS_THAN_OR_EQUAL_OP:
case AstNodeType.GREATER_THAN_OR_EQUAL_OP:
case AstNodeType.MINUS_OP:
case AstNodeType.PLUS_OP:
case AstNodeType.TIMES_OP:
case AstNodeType.DIV_OP:
case AstNodeType.POWER_OP:
return doesContainRelativeReferences(ast.left) || doesContainRelativeReferences(ast.right);
case AstNodeType.PARENTHESIS:
return doesContainRelativeReferences(ast.expression);
case AstNodeType.FUNCTION_CALL:
{
return ast.args.some(arg => doesContainRelativeReferences(arg));
}
case AstNodeType.ARRAY:
{
return ast.args.some(row => row.some(arg => doesContainRelativeReferences(arg)));
}
}
};