UNPKG

hyperformula

Version:

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

713 lines 30.6 kB
/** * @license * Copyright (c) 2025 Handsoncode. All rights reserved. */ import { EmbeddedActionsParser, EMPTY_ALT, Lexer, tokenMatcher } from 'chevrotain'; import { CellError, ErrorType, simpleCellAddress } from "../Cell.mjs"; import { ErrorMessage } from "../error-message.mjs"; import { cellAddressFromString, columnAddressFromString, rowAddressFromString } from "./addressRepresentationConverters.mjs"; import { AstNodeType, buildArrayAst, buildCellErrorAst, buildCellRangeAst, buildCellReferenceAst, buildColumnRangeAst, buildConcatenateOpAst, buildDivOpAst, buildEmptyArgAst, buildEqualsOpAst, buildErrorWithRawInputAst, buildGreaterThanOpAst, buildGreaterThanOrEqualOpAst, buildLessThanOpAst, buildLessThanOrEqualOpAst, buildMinusOpAst, buildMinusUnaryOpAst, buildNamedExpressionAst, buildNotEqualOpAst, buildNumberAst, buildParenthesisAst, buildParsingErrorAst, buildPercentOpAst, buildPlusOpAst, buildPlusUnaryOpAst, buildPowerOpAst, buildProcedureAst, buildRowRangeAst, buildStringAst, buildTimesOpAst, parsingError, ParsingErrorType, RangeSheetReferenceType } from "./Ast.mjs"; import { CellAddress, CellReferenceType } from "./CellAddress.mjs"; import { AdditionOp, ArrayLParen, ArrayRParen, BooleanOp, CellReference, ColumnRange, ConcatenateOp, DivOp, EqualsOp, ErrorLiteral, GreaterThanOp, GreaterThanOrEqualOp, LessThanOp, LessThanOrEqualOp, LParen, MinusOp, MultiplicationOp, NamedExpression, NotEqualOp, PercentOp, PlusOp, PowerOp, ProcedureName, RangeSeparator, RowRange, RParen, StringLiteral, TimesOp } from "./LexerConfig.mjs"; /** * LL(k) formula parser described using Chevrotain DSL * * It is equivalent to the grammar below: * * F -> '=' E <br/> * B -> K < B | K >= B ... | K <br/> * K -> E & K | E <br/> * E -> M + E | M - E | M <br/> * M -> W * M | W / M | W <br/> * W -> C * W | C <br/> * C -> N | R | O | A | P | num <br/> * N -> '(' E ')' <br/> * R -> A:OFFSET(..) | A:A <br/> * O -> OFFSET(..) | OFFSET(..):A | OFFSET(..):OFFSET(..) <br/> * A -> A1 | $A1 | A$1 | $A$1 <br/> * P -> SUM(..) <br/> */ export class FormulaParser extends EmbeddedActionsParser { constructor(lexerConfig, sheetMapping) { super(lexerConfig.allTokens, { outputCst: false, maxLookahead: 7 }); this.booleanExpressionOrEmpty = this.RULE('booleanExpressionOrEmpty', () => { return this.OR([{ ALT: () => this.SUBRULE(this.booleanExpression) }, { ALT: EMPTY_ALT(buildEmptyArgAst()) }]); }); /** * Rule for procedure expressions: SUM(1,A1) */ this.procedureExpression = this.RULE('procedureExpression', () => { var _a; const procedureNameToken = this.CONSUME(ProcedureName); const procedureName = procedureNameToken.image.toUpperCase().slice(0, -1); const canonicalProcedureName = (_a = this.lexerConfig.functionMapping[procedureName]) !== null && _a !== void 0 ? _a : procedureName; const args = []; let argument = this.SUBRULE(this.booleanExpressionOrEmpty); this.MANY(() => { var _a; const separator = this.CONSUME(this.lexerConfig.ArgSeparator); if (argument.type === AstNodeType.EMPTY) { argument.leadingWhitespace = (_a = separator.leadingWhitespace) === null || _a === void 0 ? void 0 : _a.image; } args.push(argument); argument = this.SUBRULE2(this.booleanExpressionOrEmpty); }); args.push(argument); if (args.length === 1 && args[0].type === AstNodeType.EMPTY) { args.length = 0; } const rParenToken = this.CONSUME(RParen); return buildProcedureAst(canonicalProcedureName, args, procedureNameToken.leadingWhitespace, rParenToken.leadingWhitespace); }); this.namedExpressionExpression = this.RULE('namedExpressionExpression', () => { const name = this.CONSUME(NamedExpression); return buildNamedExpressionAst(name.image, name.leadingWhitespace); }); /** * Rule for OFFSET() function expression */ this.offsetProcedureExpression = this.RULE('offsetProcedureExpression', () => { const args = []; this.CONSUME(this.lexerConfig.OffsetProcedureName); this.CONSUME(LParen); this.MANY_SEP({ SEP: this.lexerConfig.ArgSeparator, DEF: () => { args.push(this.SUBRULE(this.booleanExpression)); } }); this.CONSUME(RParen); return this.handleOffsetHeuristic(args); }); /** * Rule for column range, e.g., A:B, Sheet1!A:B, Sheet1!A:Sheet1!B */ this.columnRangeExpression = this.RULE('columnRangeExpression', () => { const range = this.CONSUME(ColumnRange); const [startImage, endImage] = range.image.split(':'); const firstAddress = this.ACTION(() => columnAddressFromString(this.sheetMapping, startImage, this.formulaAddress)); const secondAddress = this.ACTION(() => columnAddressFromString(this.sheetMapping, endImage, this.formulaAddress)); if (firstAddress === undefined || secondAddress === undefined) { return buildCellErrorAst(new CellError(ErrorType.REF)); } if (firstAddress.exceedsSheetSizeLimits(this.lexerConfig.maxColumns) || secondAddress.exceedsSheetSizeLimits(this.lexerConfig.maxColumns)) { return buildErrorWithRawInputAst(range.image, new CellError(ErrorType.NAME), range.leadingWhitespace); } if (firstAddress.sheet === undefined && secondAddress.sheet !== undefined) { return this.parsingError(ParsingErrorType.ParserError, 'Malformed range expression'); } const { firstEnd, secondEnd, sheetRefType } = FormulaParser.fixSheetIdsForRangeEnds(firstAddress, secondAddress); return buildColumnRangeAst(firstEnd, secondEnd, sheetRefType, range.leadingWhitespace); }); /** * Rule for row range, e.g., 1:2, Sheet1!1:2, Sheet1!1:Sheet1!2 */ this.rowRangeExpression = this.RULE('rowRangeExpression', () => { const range = this.CONSUME(RowRange); const [startImage, endImage] = range.image.split(':'); const firstAddress = this.ACTION(() => rowAddressFromString(this.sheetMapping, startImage, this.formulaAddress)); const secondAddress = this.ACTION(() => rowAddressFromString(this.sheetMapping, endImage, this.formulaAddress)); if (firstAddress === undefined || secondAddress === undefined) { return buildCellErrorAst(new CellError(ErrorType.REF)); } if (firstAddress.exceedsSheetSizeLimits(this.lexerConfig.maxRows) || secondAddress.exceedsSheetSizeLimits(this.lexerConfig.maxRows)) { return buildErrorWithRawInputAst(range.image, new CellError(ErrorType.NAME), range.leadingWhitespace); } if (firstAddress.sheet === undefined && secondAddress.sheet !== undefined) { return this.parsingError(ParsingErrorType.ParserError, 'Malformed range expression'); } const { firstEnd, secondEnd, sheetRefType } = FormulaParser.fixSheetIdsForRangeEnds(firstAddress, secondAddress); return buildRowRangeAst(firstEnd, secondEnd, sheetRefType, range.leadingWhitespace); }); /** * Rule for cell reference expression (e.g., A1, $A1, A$1, $A$1, $Sheet42!A$17) */ this.cellReference = this.RULE('cellReference', () => { const cell = this.CONSUME(CellReference); const address = this.ACTION(() => { return cellAddressFromString(this.sheetMapping, cell.image, this.formulaAddress); }); if (address === undefined) { return buildErrorWithRawInputAst(cell.image, new CellError(ErrorType.REF), cell.leadingWhitespace); } else if (address.exceedsSheetSizeLimits(this.lexerConfig.maxColumns, this.lexerConfig.maxRows)) { return buildErrorWithRawInputAst(cell.image, new CellError(ErrorType.NAME), cell.leadingWhitespace); } else { return buildCellReferenceAst(address, cell.leadingWhitespace); } }); /** * Rule for end range reference expression with additional checks considering range start */ this.endRangeReference = this.RULE('endRangeReference', start => { var _a; const end = this.CONSUME(CellReference); const startAddress = this.ACTION(() => { return cellAddressFromString(this.sheetMapping, start.image, this.formulaAddress); }); const endAddress = this.ACTION(() => { return cellAddressFromString(this.sheetMapping, end.image, this.formulaAddress); }); if (startAddress === undefined || endAddress === undefined) { return this.ACTION(() => { return buildErrorWithRawInputAst(`${start.image}:${end.image}`, new CellError(ErrorType.REF), start.leadingWhitespace); }); } else if (startAddress.exceedsSheetSizeLimits(this.lexerConfig.maxColumns, this.lexerConfig.maxRows) || endAddress.exceedsSheetSizeLimits(this.lexerConfig.maxColumns, this.lexerConfig.maxRows)) { return this.ACTION(() => { return buildErrorWithRawInputAst(`${start.image}:${end.image}`, new CellError(ErrorType.NAME), start.leadingWhitespace); }); } return this.buildCellRange(startAddress, endAddress, (_a = start.leadingWhitespace) === null || _a === void 0 ? void 0 : _a.image); }); /** * Rule for end of range expression * * End of range may be a cell reference or OFFSET() function call */ this.endOfRangeExpression = this.RULE('endOfRangeExpression', start => { return this.OR([{ ALT: () => { return this.SUBRULE(this.endRangeReference, { ARGS: [start] }); } }, { ALT: () => { var _a; const offsetProcedure = this.SUBRULE(this.offsetProcedureExpression); const startAddress = this.ACTION(() => { return cellAddressFromString(this.sheetMapping, start.image, this.formulaAddress); }); if (startAddress === undefined) { return buildCellErrorAst(new CellError(ErrorType.REF)); } if (offsetProcedure.type === AstNodeType.CELL_REFERENCE) { return this.buildCellRange(startAddress, offsetProcedure.reference, (_a = start.leadingWhitespace) === null || _a === void 0 ? void 0 : _a.image); } else { return this.parsingError(ParsingErrorType.RangeOffsetNotAllowed, 'Range offset not allowed here'); } } }]); }); /** * Rule for cell ranges (e.g., A1:B$3, A1:OFFSET()) */ this.cellRangeExpression = this.RULE('cellRangeExpression', () => { const start = this.CONSUME(CellReference); this.CONSUME2(RangeSeparator); return this.SUBRULE(this.endOfRangeExpression, { ARGS: [start] }); }); /** * Rule for end range reference expression starting with offset procedure with additional checks considering range start */ this.endRangeWithOffsetStartReference = this.RULE('endRangeWithOffsetStartReference', start => { const end = this.CONSUME(CellReference); const endAddress = this.ACTION(() => { return cellAddressFromString(this.sheetMapping, end.image, this.formulaAddress); }); if (endAddress === undefined) { return this.ACTION(() => { return buildCellErrorAst(new CellError(ErrorType.REF)); }); } return this.buildCellRange(start.reference, endAddress, start.leadingWhitespace); }); /** * Rule for end of range expression * * End of range may be a cell reference or OFFSET() function call */ this.endOfRangeWithOffsetStartExpression = this.RULE('endOfRangeWithOffsetStartExpression', start => { return this.OR([{ ALT: () => { return this.SUBRULE(this.endRangeWithOffsetStartReference, { ARGS: [start] }); } }, { ALT: () => { const offsetProcedure = this.SUBRULE(this.offsetProcedureExpression); if (offsetProcedure.type === AstNodeType.CELL_REFERENCE) { return this.buildCellRange(start.reference, offsetProcedure.reference, start.leadingWhitespace); } else { return this.parsingError(ParsingErrorType.RangeOffsetNotAllowed, 'Range offset not allowed here'); } } }]); }); /** * Rule for expressions that start with the OFFSET function. * * The OFFSET function can occur as a cell reference, or as a part of a cell range. * To preserve LL(k) properties, expressions that start with the OFFSET function need a separate rule. * * Depending on the presence of the {@link RangeSeparator}, a proper {@link Ast} node type is built. */ this.offsetExpression = this.RULE('offsetExpression', () => { const offsetProcedure = this.SUBRULE(this.offsetProcedureExpression); let end; this.OPTION(() => { this.CONSUME(RangeSeparator); if (offsetProcedure.type === AstNodeType.CELL_RANGE) { end = this.parsingError(ParsingErrorType.RangeOffsetNotAllowed, 'Range offset not allowed here'); } else { end = this.SUBRULE(this.endOfRangeWithOffsetStartExpression, { ARGS: [offsetProcedure] }); } }); if (end !== undefined) { return end; } return offsetProcedure; }); this.insideArrayExpression = this.RULE('insideArrayExpression', () => { const ret = [[]]; ret[ret.length - 1].push(this.SUBRULE(this.booleanExpression)); this.MANY(() => { this.OR([{ ALT: () => { this.CONSUME(this.lexerConfig.ArrayColSeparator); ret[ret.length - 1].push(this.SUBRULE2(this.booleanExpression)); } }, { ALT: () => { this.CONSUME(this.lexerConfig.ArrayRowSeparator); ret.push([]); ret[ret.length - 1].push(this.SUBRULE3(this.booleanExpression)); } }]); }); return buildArrayAst(ret); }); /** * Rule for parenthesis expression */ this.parenthesisExpression = this.RULE('parenthesisExpression', () => { const lParenToken = this.CONSUME(LParen); const expression = this.SUBRULE(this.booleanExpression); const rParenToken = this.CONSUME(RParen); return buildParenthesisAst(expression, lParenToken.leadingWhitespace, rParenToken.leadingWhitespace); }); this.arrayExpression = this.RULE('arrayExpression', () => { return this.OR([{ ALT: () => { const ltoken = this.CONSUME(ArrayLParen); const ret = this.SUBRULE(this.insideArrayExpression); const rtoken = this.CONSUME(ArrayRParen); return buildArrayAst(ret.args, ltoken.leadingWhitespace, rtoken.leadingWhitespace); } }, { ALT: () => this.SUBRULE(this.parenthesisExpression) }]); }); this.numericStringToNumber = input => { const normalized = input.replace(this.lexerConfig.decimalSeparator, '.'); return Number(normalized); }; /** * Rule for positive atomic expressions */ this.positiveAtomicExpression = this.RULE('positiveAtomicExpression', () => { var _a; return this.OR((_a = this.atomicExpCache) !== null && _a !== void 0 ? _a : this.atomicExpCache = [{ ALT: () => this.SUBRULE(this.arrayExpression) }, { ALT: () => this.SUBRULE(this.cellRangeExpression) }, { ALT: () => this.SUBRULE(this.columnRangeExpression) }, { ALT: () => this.SUBRULE(this.rowRangeExpression) }, { ALT: () => this.SUBRULE(this.offsetExpression) }, { ALT: () => this.SUBRULE(this.cellReference) }, { ALT: () => this.SUBRULE(this.procedureExpression) }, { ALT: () => this.SUBRULE(this.namedExpressionExpression) }, { ALT: () => { const number = this.CONSUME(this.lexerConfig.NumberLiteral); return buildNumberAst(this.numericStringToNumber(number.image), number.leadingWhitespace); } }, { ALT: () => { const str = this.CONSUME(StringLiteral); return buildStringAst(str); } }, { ALT: () => { const token = this.CONSUME(ErrorLiteral); const errString = token.image.toUpperCase(); const errorType = this.lexerConfig.errorMapping[errString]; if (errorType) { return buildCellErrorAst(new CellError(errorType), token.leadingWhitespace); } else { return this.parsingError(ParsingErrorType.ParserError, 'Unknown error literal'); } } }]); }); this.rightUnaryOpAtomicExpression = this.RULE('rightUnaryOpAtomicExpression', () => { const positiveAtomicExpression = this.SUBRULE(this.positiveAtomicExpression); const percentage = this.OPTION(() => { return this.CONSUME(PercentOp); }); if (percentage) { return buildPercentOpAst(positiveAtomicExpression, percentage.leadingWhitespace); } return positiveAtomicExpression; }); /** * Rule for atomic expressions, which is positive atomic expression or negation of it */ this.atomicExpression = this.RULE('atomicExpression', () => { return this.OR([{ ALT: () => { const op = this.CONSUME(AdditionOp); const value = this.SUBRULE(this.atomicExpression); if (tokenMatcher(op, PlusOp)) { return buildPlusUnaryOpAst(value, op.leadingWhitespace); } else if (tokenMatcher(op, MinusOp)) { return buildMinusUnaryOpAst(value, op.leadingWhitespace); } else { this.customParsingError = parsingError(ParsingErrorType.ParserError, 'Mismatched token type'); return this.customParsingError; } } }, { ALT: () => this.SUBRULE2(this.rightUnaryOpAtomicExpression) }]); }); /** * Rule for power expression */ this.powerExpression = this.RULE('powerExpression', () => { let lhs = this.SUBRULE(this.atomicExpression); this.MANY(() => { const op = this.CONSUME(PowerOp); const rhs = this.SUBRULE2(this.atomicExpression); if (tokenMatcher(op, PowerOp)) { lhs = buildPowerOpAst(lhs, rhs, op.leadingWhitespace); } else { this.ACTION(() => { throw Error('Operator not supported'); }); } }); return lhs; }); /** * Rule for multiplication category operators (e.g., 1 * A1, 1 / A1) */ this.multiplicationExpression = this.RULE('multiplicationExpression', () => { let lhs = this.SUBRULE(this.powerExpression); this.MANY(() => { const op = this.CONSUME(MultiplicationOp); const rhs = this.SUBRULE2(this.powerExpression); if (tokenMatcher(op, TimesOp)) { lhs = buildTimesOpAst(lhs, rhs, op.leadingWhitespace); } else if (tokenMatcher(op, DivOp)) { lhs = buildDivOpAst(lhs, rhs, op.leadingWhitespace); } else { this.ACTION(() => { throw Error('Operator not supported'); }); } }); return lhs; }); /** * Rule for addition category operators (e.g., 1 + A1, 1 - A1) */ this.additionExpression = this.RULE('additionExpression', () => { let lhs = this.SUBRULE(this.multiplicationExpression); this.MANY(() => { const op = this.CONSUME(AdditionOp); const rhs = this.SUBRULE2(this.multiplicationExpression); if (tokenMatcher(op, PlusOp)) { lhs = buildPlusOpAst(lhs, rhs, op.leadingWhitespace); } else if (tokenMatcher(op, MinusOp)) { lhs = buildMinusOpAst(lhs, rhs, op.leadingWhitespace); } else { this.ACTION(() => { throw Error('Operator not supported'); }); } }); return lhs; }); /** * Rule for concatenation operator expression (e.g., "=" & A1) */ this.concatenateExpression = this.RULE('concatenateExpression', () => { let lhs = this.SUBRULE(this.additionExpression); this.MANY(() => { const op = this.CONSUME(ConcatenateOp); const rhs = this.SUBRULE2(this.additionExpression); lhs = buildConcatenateOpAst(lhs, rhs, op.leadingWhitespace); }); return lhs; }); /** * Rule for boolean expression (e.g., 1 <= A1) */ this.booleanExpression = this.RULE('booleanExpression', () => { let lhs = this.SUBRULE(this.concatenateExpression); this.MANY(() => { const op = this.CONSUME(BooleanOp); const rhs = this.SUBRULE2(this.concatenateExpression); if (tokenMatcher(op, EqualsOp)) { lhs = buildEqualsOpAst(lhs, rhs, op.leadingWhitespace); } else if (tokenMatcher(op, NotEqualOp)) { lhs = buildNotEqualOpAst(lhs, rhs, op.leadingWhitespace); } else if (tokenMatcher(op, GreaterThanOp)) { lhs = buildGreaterThanOpAst(lhs, rhs, op.leadingWhitespace); } else if (tokenMatcher(op, LessThanOp)) { lhs = buildLessThanOpAst(lhs, rhs, op.leadingWhitespace); } else if (tokenMatcher(op, GreaterThanOrEqualOp)) { lhs = buildGreaterThanOrEqualOpAst(lhs, rhs, op.leadingWhitespace); } else if (tokenMatcher(op, LessThanOrEqualOp)) { lhs = buildLessThanOrEqualOpAst(lhs, rhs, op.leadingWhitespace); } else { this.ACTION(() => { throw Error('Operator not supported'); }); } }); return lhs; }); /** * Entry rule */ this.formula = this.RULE('formula', () => { this.CONSUME(EqualsOp); return this.SUBRULE(this.booleanExpression); }); this.lexerConfig = lexerConfig; this.sheetMapping = sheetMapping; this.formulaAddress = simpleCellAddress(0, 0, 0); this.performSelfAnalysis(); } /** * Parses tokenized formula and builds abstract syntax tree * * @param {ExtendedToken[]} tokens - tokenized formula * @param {SimpleCellAddress} formulaAddress - address of the cell in which formula is located */ parseFromTokens(tokens, formulaAddress) { this.input = tokens; let ast = this.formulaWithContext(formulaAddress); let errors = []; if (this.customParsingError) { errors.push(this.customParsingError); } errors = errors.concat(this.errors.map(e => ({ type: ParsingErrorType.ParserError, message: e.message }))); if (errors.length > 0) { ast = buildParsingErrorAst(); } return { ast, errors }; } reset() { super.reset(); this.customParsingError = undefined; } /** * Entry rule wrapper that sets formula address * * @param {SimpleCellAddress} address - address of the cell in which formula is located */ formulaWithContext(address) { this.formulaAddress = address; return this.formula(); } buildCellRange(firstAddress, secondAddress, leadingWhitespace) { if (firstAddress.sheet === undefined && secondAddress.sheet !== undefined) { return this.parsingError(ParsingErrorType.ParserError, 'Malformed range expression'); } const { firstEnd, secondEnd, sheetRefType } = FormulaParser.fixSheetIdsForRangeEnds(firstAddress, secondAddress); return buildCellRangeAst(firstEnd, secondEnd, sheetRefType, leadingWhitespace); } static fixSheetIdsForRangeEnds(firstEnd, secondEnd) { const sheetRefType = FormulaParser.rangeSheetReferenceType(firstEnd.sheet, secondEnd.sheet); const secondEndFixed = firstEnd.sheet !== undefined && secondEnd.sheet === undefined ? secondEnd.withSheet(firstEnd.sheet) : secondEnd; return { firstEnd, secondEnd: secondEndFixed, sheetRefType }; } /** * Returns {@link CellReferenceAst} or {@link CellRangeAst} based on OFFSET function arguments * * @param {Ast[]} args - OFFSET function arguments */ handleOffsetHeuristic(args) { const cellArg = args[0]; if (cellArg.type !== AstNodeType.CELL_REFERENCE) { return this.parsingError(ParsingErrorType.StaticOffsetError, 'First argument to OFFSET is not a reference'); } const rowsArg = args[1]; let rowShift; if (rowsArg.type === AstNodeType.NUMBER && Number.isInteger(rowsArg.value)) { rowShift = rowsArg.value; } else if (rowsArg.type === AstNodeType.PLUS_UNARY_OP && rowsArg.value.type === AstNodeType.NUMBER && Number.isInteger(rowsArg.value.value)) { rowShift = rowsArg.value.value; } else if (rowsArg.type === AstNodeType.MINUS_UNARY_OP && rowsArg.value.type === AstNodeType.NUMBER && Number.isInteger(rowsArg.value.value)) { rowShift = -rowsArg.value.value; } else { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Second argument to OFFSET is not a static number'); } const columnsArg = args[2]; let colShift; if (columnsArg.type === AstNodeType.NUMBER && Number.isInteger(columnsArg.value)) { colShift = columnsArg.value; } else if (columnsArg.type === AstNodeType.PLUS_UNARY_OP && columnsArg.value.type === AstNodeType.NUMBER && Number.isInteger(columnsArg.value.value)) { colShift = columnsArg.value.value; } else if (columnsArg.type === AstNodeType.MINUS_UNARY_OP && columnsArg.value.type === AstNodeType.NUMBER && Number.isInteger(columnsArg.value.value)) { colShift = -columnsArg.value.value; } else { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Third argument to OFFSET is not a static number'); } const heightArg = args[3]; let height; if (heightArg === undefined) { height = 1; } else if (heightArg.type === AstNodeType.NUMBER) { height = heightArg.value; if (height < 1) { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Fourth argument to OFFSET is too small number'); } else if (!Number.isInteger(height)) { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Fourth argument to OFFSET is not integer'); } } else { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Fourth argument to OFFSET is not a static number'); } const widthArg = args[4]; let width; if (widthArg === undefined) { width = 1; } else if (widthArg.type === AstNodeType.NUMBER) { width = widthArg.value; if (width < 1) { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Fifth argument to OFFSET is too small number'); } else if (!Number.isInteger(width)) { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Fifth argument to OFFSET is not integer'); } } else { return this.parsingError(ParsingErrorType.StaticOffsetError, 'Fifth argument to OFFSET is not a static number'); } const topLeftCorner = new CellAddress(cellArg.reference.col + colShift, cellArg.reference.row + rowShift, cellArg.reference.type); let absoluteCol = topLeftCorner.col; let absoluteRow = topLeftCorner.row; if (cellArg.reference.type === CellReferenceType.CELL_REFERENCE_RELATIVE || cellArg.reference.type === CellReferenceType.CELL_REFERENCE_ABSOLUTE_COL) { absoluteRow = absoluteRow + this.formulaAddress.row; } if (cellArg.reference.type === CellReferenceType.CELL_REFERENCE_RELATIVE || cellArg.reference.type === CellReferenceType.CELL_REFERENCE_ABSOLUTE_ROW) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion absoluteCol = absoluteCol + this.formulaAddress.col; } if (absoluteCol < 0 || absoluteRow < 0) { return buildCellErrorAst(new CellError(ErrorType.REF, ErrorMessage.OutOfSheet)); } if (width === 1 && height === 1) { return buildCellReferenceAst(topLeftCorner); } else { const bottomRightCorner = new CellAddress(topLeftCorner.col + width - 1, topLeftCorner.row + height - 1, topLeftCorner.type); return buildCellRangeAst(topLeftCorner, bottomRightCorner, RangeSheetReferenceType.RELATIVE); } } parsingError(type, message) { this.customParsingError = parsingError(type, message); return buildParsingErrorAst(); } static rangeSheetReferenceType(start, end) { if (start === undefined) { return RangeSheetReferenceType.RELATIVE; } else if (end === undefined) { return RangeSheetReferenceType.START_ABSOLUTE; } else { return RangeSheetReferenceType.BOTH_ABSOLUTE; } } } export class FormulaLexer { constructor(lexerConfig) { this.lexerConfig = lexerConfig; this.lexer = new Lexer(lexerConfig.allTokens, { ensureOptimizations: true }); } /** * Returns Lexer tokens from formula string * * @param {string} text - string representation of a formula */ tokenizeFormula(text) { const lexingResult = this.lexer.tokenize(text); let tokens = lexingResult.tokens; tokens = this.trimTrailingWhitespaces(tokens); tokens = this.skipWhitespacesInsideRanges(tokens); tokens = this.skipWhitespacesBeforeArgSeparators(tokens); lexingResult.tokens = tokens; return lexingResult; } skipWhitespacesInsideRanges(tokens) { return FormulaLexer.filterTokensByNeighbors(tokens, (previous, current, next) => { return (tokenMatcher(previous, CellReference) || tokenMatcher(previous, RangeSeparator)) && tokenMatcher(current, this.lexerConfig.WhiteSpace) && (tokenMatcher(next, CellReference) || tokenMatcher(next, RangeSeparator)); }); } skipWhitespacesBeforeArgSeparators(tokens) { return FormulaLexer.filterTokensByNeighbors(tokens, (previous, current, next) => { return !tokenMatcher(previous, this.lexerConfig.ArgSeparator) && tokenMatcher(current, this.lexerConfig.WhiteSpace) && tokenMatcher(next, this.lexerConfig.ArgSeparator); }); } static filterTokensByNeighbors(tokens, shouldBeSkipped) { if (tokens.length < 3) { return tokens; } let i = 0; const filteredTokens = [tokens[i++]]; while (i < tokens.length - 1) { if (!shouldBeSkipped(tokens[i - 1], tokens[i], tokens[i + 1])) { filteredTokens.push(tokens[i]); } ++i; } filteredTokens.push(tokens[i]); return filteredTokens; } trimTrailingWhitespaces(tokens) { if (tokens.length > 0 && tokenMatcher(tokens[tokens.length - 1], this.lexerConfig.WhiteSpace)) { tokens.pop(); } return tokens; } }