diff --git a/README.md b/README.md index 27a89183..6685f731 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,52 @@ Note: The grammar in my implementation is different from theirs. My implementati ]; ``` + - Replace Formula References + > This is helpful for renaming/rearranging columns/rows/cell/variables even for cut/paste implementaions. + ```js + import {RefParser} from 'fast-formula-parser'; + const refParser = new RefParser(); + + // position of the formula should be provided + const position = {row: 1, col: 1, sheet: 'Sheet1'}; + + // Return formula with replaced column/row/cell coordinates even variable names + // This gives 'X100+1' + refParser.replace('A1+1', position, [{ + type: 'col', from: 1, to: 24, + }, { + type: 'row', from: 1, to: 100, + } ]); + + // This gives 'X100:C100' + refParser.replace('A1:C1', position, [ { + type: 'col', from: 1, to: 24, + }, { + type: 'row', from: 1, to: 100, + } ]); + + // This gives 'X100:C3' + refParser.replace('A1:C3', position, [ { + type: 'cell', + from: { col: 1, row: 1, }, + to: { col: 24, row: 100, } + }]); + + // This gives 'var123 + 1' + refParser.replace('VAR1 + 1', position, [ { type: 'variable', from: 'VAR1', to: 'var123' } ]); + + // Complex formula + refParser.replace('IF(MONTH($K$1)<>MONTH($K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1)),"",$K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1))', position, [{ + type: 'col', from: 1, to: 24, + }, { + type: 'row', from: 1, to: 100, + }, { + type: 'variable', from: 'start_day', to: 'first_day' + }]); + // This gives the following result + const result = 'IF(MONTH($K$100)<>MONTH($K$100-(WEEKDAY($K$100,1)-(first_day-1))-IF((WEEKDAY($K$100,1)-(first_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1)),"",$K$100-(WEEKDAY($K$100,1)-(first_day-1))-IF((WEEKDAY($K$100,1)-(first_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1))'; + ``` + ### Formula data types in JavaScript > The following data types are used in excel formulas and these are the only valid data types a formula or a function can return. - Number (date uses number): `1234` diff --git a/grammar/parsing.js b/grammar/parsing.js index 6ffe3b02..04f2b4d6 100644 --- a/grammar/parsing.js +++ b/grammar/parsing.js @@ -269,8 +269,8 @@ class Parsing extends EmbeddedActionsParser { $.RULE('constant', () => $.OR([ { ALT: () => { - const number = $.CONSUME(Number).image; - return $.ACTION(() => this.utils.toNumber(number)); + const number = $.CONSUME(Number); + return $.ACTION(() => this.utils.toNumber(number.image, number.startOffset, number.endOffset)); } }, { ALT: () => { @@ -350,20 +350,21 @@ class Parsing extends EmbeddedActionsParser { $.RULE('referenceItem', () => $.OR([ { ALT: () => { - const address = $.CONSUME(Cell).image; - return $.ACTION(() => this.utils.parseCellAddress(address)); + const address = $.CONSUME(Cell); + return $.ACTION(() => this.utils.parseCellAddress(address.image, address.startOffset, address.endOffset)); } }, { ALT: () => { - const name = $.CONSUME(Name).image; - return $.ACTION(() => context.getVariable(name)) + const name = $.CONSUME(Name); + $.ACTION(() => this.utils.registerVariable && this.utils.registerVariable(name.image, name.startOffset, name.endOffset)); + return $.ACTION(() => context.getVariable(name.image)) } }, { ALT: () => { - const column = $.CONSUME(Column).image; - return $.ACTION(() => this.utils.parseCol(column)) + const column = $.CONSUME(Column); + return $.ACTION(() => this.utils.parseCol(column.image, column.startOffset, column.endOffset)); } }, // A row check should be here, but the token is same with Number, diff --git a/grammar/references/hooks.js b/grammar/references/hooks.js new file mode 100644 index 00000000..0090099d --- /dev/null +++ b/grammar/references/hooks.js @@ -0,0 +1,224 @@ +const FormulaError = require('../../formulas/error'); +const {FormulaHelpers} = require('../../formulas/helpers'); +const {Parser} = require('../parsing'); +const lexer = require('../lexing'); +const Utils = require('./utils'); +const {formatChevrotainError} = require('../utils'); + +class RefParser { + + /** + * + * @param {} + */ + constructor() { + this.data = []; + this.utils = new Utils(this); + + this.parser = new Parser(this, this.utils); + } + + /** + * Get value from the cell reference + * @param ref + * @return {*} + */ + getCell(ref) { + // console.log('get cell', JSON.stringify(ref)); + return 0; + } + + /** + * Get values from the range reference. + * @param ref + * @return {*} + */ + getRange(ref) { + // console.log('get range', JSON.stringify(ref)); + return [[0]] + } + + /** + * TODO: + * Get references or values from a user defined variable. + * @param name + * @return {*} + */ + getVariable(name) { + // console.log('get variable', name); + return 0; + } + + /** + * Retrieve values from the given reference. + * @param valueOrRef + * @return {*} + */ + retrieveRef(valueOrRef) { + if (FormulaHelpers.isRangeRef(valueOrRef)) { + return this.getRange(valueOrRef.ref); + } + if (FormulaHelpers.isCellRef(valueOrRef)) { + return this.getCell(valueOrRef.ref) + } + return valueOrRef; + } + + /** + * Call an excel function. + * @param name - Function name. + * @param args - Arguments that pass to the function. + * @return {*} + */ + callFunction(name, args) { + return {value: 0, ref: {}}; + } + + /** + * Check and return the appropriate formula result. + * @param result + * @return {*} + */ + checkFormulaResult(result) { + this.retrieveRef(result); + } + + /** + * Parse an excel formula and return the column/row/cell/variable references + * @param {string} inputText + * @param {{row: number, col: number, sheet: string}} position + * @param {boolean} [ignoreError=false] if true, throw FormulaError when error occurred. + * if false, the parser will return partial references. + * @returns {Array.<{}>} + */ + parse(inputText, position, ignoreError = false) { + if (inputText.length === 0) throw Error('Input must not be empty.'); + this.data = []; + this.position = position; + const lexResult = lexer.lex(inputText); + this.parser.input = lexResult.tokens; + try { + const res = this.parser.formulaWithBinaryOp(); + this.checkFormulaResult(res); + } catch (e) { + if (!ignoreError) { + throw FormulaError.ERROR(e.message, e); + } + } + if (this.parser.errors.length > 0 && !ignoreError) { + const error = this.parser.errors[0]; + throw formatChevrotainError(error, inputText); + } + + return this.data; + } + + /** + * Replace column/row/cell/variable references in formula + * @param {string} inputText + * @param {{row: number, col: number, sheet: string}} position + * @param {({type: 'row' | 'column', from: number, to?: number} | {type: 'variable', from: string, to?: string} | {type: 'cell', from: {row: number, col: number}, to?: {row: number, col: number}})[] script + * @param {boolean} [ignoreError=false] if true, throw FormulaError when error occurred. + * if false, the parser will return partial references. + * @returns {Array.<{}>} + */ + replace(inputText, position, script, ignoreError = false) { + if (inputText.length === 0) throw Error('Input must not be empty.'); + + const references = this.parse(inputText, position, ignoreError); + + const changes = []; + for(let index = 0; index < script.length; index++) { + const command = script[index]; + + const processOneCommand = (flattenWith, to, order) => { + changes.push(...references + .map(flattenWith) + .filter((reference) => reference !== undefined) + .map((reference) => ({ ...reference, to, order }))); + }; + + switch( command.type ) { + case 'row': + processOneCommand( + (reference) => + reference.type === 'row' ? reference.ref.row === command.from ? reference : undefined : + reference.type === 'cell' ? reference.ref.row === command.from ? reference.row : undefined : + undefined, + command.to == null ? undefined : command.to.toString(), + index + ); + if( command.to == null ) { + processOneCommand( + (reference) => + reference.type === 'cell' ? reference.ref.row === command.from ? reference.col : undefined : + undefined, + '', + index + ); + } + break; + case 'col': + processOneCommand( + (reference) => + reference.type === 'col' ? reference.ref.col === command.from ? reference : undefined : + reference.type === 'cell' ? reference.ref.col === command.from ? reference.col : undefined : + undefined, + command.to == null ? undefined : this.utils.columnNumberToName(command.to), + index + ); + if( command.to == null ) { + processOneCommand( + (reference) => + reference.type === 'cell' ? reference.ref.col === command.from ? reference.row : undefined : + undefined, + '', + index + ); + } + break; + case 'cell': + processOneCommand( + (reference) => + reference.type === 'cell' ? + reference.ref.row === command.from.row && reference.ref.col === command.from.col ? reference : undefined : + undefined, + command.to == null ? undefined : `${this.utils.columnNumberToName(command.to.col)}${command.to.row}`, + index + ); + break; + case 'variable': + processOneCommand( + (reference) => + reference.type === 'variable' ? reference.name === command.from ? reference : undefined : + undefined, + command.to, + index + ); + break; + default: + throw new Error(`Invalid script command type "${command.type}"`); + } + } + + changes.sort((a, b) => + a.startOffset < b.startOffset ? 1 : + a.startOffset > b.startOffset ? -1 : + a.endOffset < b.endOffset ? 1 : + a.endOffset > b.endOffset ? -1 : + a.index < b.index ? -1 : + a.index > b.index ? 1 : + 0 + ); + + for(const item of changes) { + inputText = inputText.substring(0, item.startOffset) + (item.to == null ? '#REF!' : item.to) + inputText.substring(item.endOffset + 1); + } + + return inputText; + } +} + +module.exports = { + RefParser, +}; diff --git a/grammar/references/utils.js b/grammar/references/utils.js new file mode 100644 index 00000000..783557d1 --- /dev/null +++ b/grammar/references/utils.js @@ -0,0 +1,336 @@ +const FormulaError = require('../../formulas/error'); +const { Address} = require('../../formulas/helpers'); +const Collection = require('../type/collection'); +const MAX_ROW = 1048576, MAX_COLUMN = 16384; + +class Utils { + + constructor(context) { + this.context = context; + } + + columnNumberToName(columnNumber) { + return Address.columnNumberToName(columnNumber); + } + + columnNameToNumber(columnName) { + return Address.columnNameToNumber(columnName); + } + + /** + * Parse the cell address only. + * @param {string} cellAddress + * @return {{ref: {col: number, address: string, row: number}}} + */ + parseCellAddress(cellAddress, startOffset, endOffset) { + const res = cellAddress.match(/([$]?)([A-Za-z]{1,3})([$]?)([1-9][0-9]*)/); + // console.log('parseCellAddress', cellAddress); + const ref = { + col: this.columnNameToNumber(res[2]), + row: +res[4] + }; + this.context.data.push({ + type: 'cell', + cellAddress, + col: { + col: res[2], + startOffset: startOffset + res[1].length, + endOffset: startOffset + res[1].length + res[2].length - 1, + }, + row: { + row: +res[4], + startOffset: startOffset + res[1].length + res[2].length + res[3].length, + endOffset: startOffset + res[1].length + res[2].length + res[3].length + res[4].length - 1, + }, + startOffset, + endOffset, + ref, + }); + return { ref }; + } + + parseRow(row, startOffset, endOffset) { + const rowNum = +row; + if (!Number.isInteger(rowNum)) + throw Error('Row number must be integer.'); + const ref = { + col: undefined, + row: +row + }; + this.context.data.push({ + type: 'row', + row: rowNum, + startOffset, + endOffset, + ref, + }); + return { ref }; + } + + parseCol(col, startOffset, endOffset) { + const ref = { + col: this.columnNameToNumber(col), + row: undefined, + }; + this.context.data.push({ + type: 'col', + col, + startOffset, + endOffset, + ref, + }); + return { ref }; + } + + registerVariable(name, startOffset, endOffset) { + this.context.data.push({ + type: 'variable', + name, + startOffset, + endOffset, + }); + } + + /** + * Apply + or - unary prefix. + * @param {Array.} prefixes + * @param {*} value + * @return {*} + */ + applyPrefix(prefixes, value) { + this.extractRefValue(value); + return 0; + } + + applyPostfix(value, postfix) { + this.extractRefValue(value); + return 0 + } + + applyInfix(value1, infix, value2) { + this.extractRefValue(value1); + this.extractRefValue(value2); + return 0; + } + + applyIntersect(refs) { + // console.log('applyIntersect', refs); + if (this.isFormulaError(refs[0])) + return refs[0]; + if (!refs[0].ref) + throw Error(`Expecting a reference, but got ${refs[0]}.`); + // a intersection will keep track of references, value won't be retrieved here. + let maxRow, maxCol, minRow, minCol, sheet, res; // index start from 1 + // first time setup + const ref = refs.shift().ref; + sheet = ref.sheet; + if (!ref.from) { + // check whole row/col reference + if (ref.row === undefined || ref.col === undefined) { + throw Error('Cannot intersect the whole row or column.') + } + + // cell ref + maxRow = minRow = ref.row; + maxCol = minCol = ref.col; + } else { + // range ref + // update + maxRow = Math.max(ref.from.row, ref.to.row); + minRow = Math.min(ref.from.row, ref.to.row); + maxCol = Math.max(ref.from.col, ref.to.col); + minCol = Math.min(ref.from.col, ref.to.col); + } + + let err; + refs.forEach(ref => { + if (this.isFormulaError(ref)) + return ref; + ref = ref.ref; + if (!ref) throw Error(`Expecting a reference, but got ${ref}.`); + if (!ref.from) { + if (ref.row === undefined || ref.col === undefined) { + throw Error('Cannot intersect the whole row or column.') + } + // cell ref + if (ref.row > maxRow || ref.row < minRow || ref.col > maxCol || ref.col < minCol + || sheet !== ref.sheet) { + err = FormulaError.NULL; + } + maxRow = minRow = ref.row; + maxCol = minCol = ref.col; + } else { + // range ref + const refMaxRow = Math.max(ref.from.row, ref.to.row); + const refMinRow = Math.min(ref.from.row, ref.to.row); + const refMaxCol = Math.max(ref.from.col, ref.to.col); + const refMinCol = Math.min(ref.from.col, ref.to.col); + if (refMinRow > maxRow || refMaxRow < minRow || refMinCol > maxCol || refMaxCol < minCol + || sheet !== ref.sheet) { + err = FormulaError.NULL; + } + // update + maxRow = Math.min(maxRow, refMaxRow); + minRow = Math.max(minRow, refMinRow); + maxCol = Math.min(maxCol, refMaxCol); + minCol = Math.max(minCol, refMinCol); + } + }); + if (err) return err; + // check if the ref can be reduced to cell reference + if (maxRow === minRow && maxCol === minCol) { + res = { + ref: { + sheet, + row: maxRow, + col: maxCol + } + } + } else { + res = { + ref: { + sheet, + from: {row: minRow, col: minCol}, + to: {row: maxRow, col: maxCol} + } + }; + } + + if (!res.ref.sheet) + delete res.ref.sheet; + return res; + } + + applyUnion(refs) { + const collection = new Collection(); + for (let i = 0; i < refs.length; i++) { + if (this.isFormulaError(refs[i])) + return refs[i]; + collection.add(this.extractRefValue(refs[i]).val, refs[i]); + } + + // console.log('applyUnion', unions); + return collection; + } + + /** + * Apply multiple references, e.g. A1:B3:C8:A:1:..... + * @param refs + // * @return {{ref: {from: {col: number, row: number}, to: {col: number, row: number}}}} + */ + applyRange(refs) { + let res, maxRow = -1, maxCol = -1, minRow = MAX_ROW + 1, minCol = MAX_COLUMN + 1; + refs.forEach(ref => { + if (this.isFormulaError(ref)) + return ref; + // row ref is saved as number, parse the number to row ref here + if (typeof ref === 'object' && 'number' in ref) { + ref = this.parseRow(ref.number, ref.startOffset, ref.endOffset); + } + ref = ref.ref; + // check whole row/col reference + if (ref.row === undefined) { + minRow = 1; + maxRow = MAX_ROW + } + if (ref.col === undefined) { + minCol = 1; + maxCol = MAX_COLUMN; + } + + if (ref.row > maxRow) + maxRow = ref.row; + if (ref.row < minRow) + minRow = ref.row; + if (ref.col > maxCol) + maxCol = ref.col; + if (ref.col < minCol) + minCol = ref.col; + }); + if (maxRow === minRow && maxCol === minCol) { + res = { + ref: { + row: maxRow, + col: maxCol + } + } + } else { + res = { + ref: { + from: {row: minRow, col: minCol}, + to: {row: maxRow, col: maxCol} + } + }; + } + return res; + } + + /** + * Throw away the refs, and retrieve the value. + * @return {{val: *, isArray: boolean}} + */ + extractRefValue(obj) { + const isArray = Array.isArray(obj); + if (obj.ref) { + // can be number or array + return {val: this.context.retrieveRef(obj), isArray}; + + } + return {val: obj, isArray}; + } + + /** + * + * @param array + * @return {Array} + */ + toArray(array) { + // TODO: check if array is valid + // console.log('toArray', array); + return array; + } + + /** + * @param {string} number + * @return {{ type: 'number', number: Number(number), startOffset: number, endOffset: number }} + */ + toNumber(number, startOffset, endOffset) { + return { + type: 'number', + number: Number(number), + startOffset, + endOffset, + }; + } + + /** + * @param {string} string + * @return {string} + */ + toString(string) { + return string.substring(1, string.length - 1) .replace(/""/g, '"'); + } + + /** + * @param {string} bool + * @return {boolean} + */ + toBoolean(bool) { + return bool === 'TRUE'; + } + + /** + * Parse an error. + * @param {string} error + * @return {FormulaError} + */ + toError(error) { + return new FormulaError(error.toUpperCase()); + } + + isFormulaError(obj) { + return obj instanceof FormulaError; + } +} + +module.exports = Utils; diff --git a/index.js b/index.js index bed03ed1..3b667b26 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ const {FormulaParser} = require('./grammar/hooks'); const {DepParser} = require('./grammar/dependency/hooks'); +const {RefParser} = require('./grammar/references/hooks'); const SSF = require('./ssf/ssf'); const FormulaError = require('./formulas/error'); @@ -13,6 +14,7 @@ Object.assign(FormulaParser, { MAX_COLUMN: 16384, SSF, DepParser, + RefParser, FormulaError, ...require('./formulas/helpers') }); module.exports = FormulaParser; diff --git a/test/grammar/errors.js b/test/grammar/errors.js index ba977df4..2f300bb3 100644 --- a/test/grammar/errors.js +++ b/test/grammar/errors.js @@ -2,6 +2,7 @@ const expect = require('chai').expect; const FormulaError = require('../../formulas/error'); const {FormulaParser} = require('../../grammar/hooks'); const {DepParser} = require('../../grammar/dependency/hooks'); +const {RefParser} = require('../../grammar/references/hooks'); const {MAX_ROW, MAX_COLUMN} = require('../../index'); const parser = new FormulaParser({ @@ -32,8 +33,14 @@ const depParser = new DepParser({ } }); -const parsers = [parser, depParser]; -const names = ['', ' (DepParser)'] +const refParser = new RefParser({ + onVariable: variable => { + return 'aaaa' === variable ? {from: {row: 1, col: 1}, to: {row: 2, col: 2}} : {row: 1, col: 1}; + } +}); + +const parsers = [parser, depParser, refParser]; +const names = ['', ' (DepParser)', ' (RefParser)'] const position = {row: 1, col: 1, sheet: 'Sheet1'}; @@ -122,4 +129,12 @@ describe('#ERROR! Error handling', () => { throw Error('Should not reach here.'); } }); + + it('should not throw error when ignoreError = true (RefParser)', function () { + try { + refParser.parse('SUM(*()', position, true); + } catch (e) { + throw Error('Should not reach here.'); + } + }); }); diff --git a/test/grammar/refParser.js b/test/grammar/refParser.js new file mode 100644 index 00000000..6824af90 --- /dev/null +++ b/test/grammar/refParser.js @@ -0,0 +1,1202 @@ +const expect = require('chai').expect; +const {RefParser} = require('../../grammar/references/hooks'); + +const refParser = new RefParser(); +const position = {row: 1, col: 1, sheet: 'Sheet1'}; + +describe('References parser', () => { + it('parse SUM(1,)', () => { + let actual = refParser.parse('SUM(1,)', position); + expect(actual).to.deep.eq([]); + }); + + it('should parse single cell', () => { + let actual = refParser.parse('A1', position); + expect(actual).to.deep.eq([{ + type: 'cell', + cellAddress: 'A1', + col: { + col: 'A', + endOffset: 0, + startOffset: 0, + }, + endOffset: 1, + ref: { + col: 1, + row: 1, + }, + row: { + row: 1, + endOffset: 1, + startOffset: 1, + }, + startOffset: 0, + }]); + actual = refParser.parse('A1+1', position); + expect(actual).to.deep.eq([{ + type: 'cell', + cellAddress: 'A1', + col: { + col: 'A', + endOffset: 0, + startOffset: 0, + }, + endOffset: 1, + ref: { + col: 1, + row: 1, + }, + row: { + row: 1, + endOffset: 1, + startOffset: 1, + }, + startOffset: 0, + }]); + }); + + it('should parse the same cell/range multiple times', () => { + let actual = refParser.parse('A1+A1+A1', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 0, + "startOffset": 0, + }, + "endOffset": 1, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 1, + "row": 1, + "startOffset": 1, + }, + "startOffset": 0, + "type": "cell", + }, { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 3, + "startOffset": 3, + }, + "endOffset": 4, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 4, + "row": 1, + "startOffset": 4, + }, + "startOffset": 3, + "type": "cell", + }, { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 6, + "startOffset": 6, + }, + "endOffset": 7, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 7, + "row": 1, + "startOffset": 7, + }, + "startOffset": 6, + "type": "cell", + }]); + actual = refParser.parse('A1:C3+A1:C3+A1:C3', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 0, + "startOffset": 0, + }, + "endOffset": 1, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 1, + "row": 1, + "startOffset": 1, + }, + "startOffset": 0, + "type": "cell", + }, { + "cellAddress": "C3", + "col": { + "col": "C", + "endOffset": 3, + "startOffset": 3, + }, + "endOffset": 4, + "ref": { + "col": 3, + "row": 3, + }, + "row": { + "endOffset": 4, + "row": 3, + "startOffset": 4, + }, + "startOffset": 3, + "type": "cell", + }, { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 6, + "startOffset": 6, + }, + "endOffset": 7, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 7, + "row": 1, + "startOffset": 7, + }, + "startOffset": 6, + "type": "cell", + }, { + "cellAddress": "C3", + "col": { + "col": "C", + "endOffset": 9, + "startOffset": 9, + }, + "endOffset": 10, + "ref": { + "col": 3, + "row": 3, + }, + "row": { + "endOffset": 10, + "row": 3, + "startOffset": 10, + }, + "startOffset": 9, + "type": "cell", + }, { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 12, + "startOffset": 12, + }, + "endOffset": 13, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 13, + "row": 1, + "startOffset": 13, + }, + "startOffset": 12, + "type": "cell", + }, { + "cellAddress": "C3", + "col": { + "col": "C", + "endOffset": 15, + "startOffset": 15, + }, + "endOffset": 16, + "ref": { + "col": 3, + "row": 3, + }, + "row": { + "endOffset": 16, + "row": 3, + "startOffset": 16, + }, + "startOffset": 15, + "type": "cell", + }]); + + actual = refParser.parse('A1:C3+A1:C3+A1:C3+A1+B1', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 0, + "startOffset": 0, + }, + "endOffset": 1, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 1, + "row": 1, + "startOffset": 1, + }, + "startOffset": 0, + "type": "cell", + }, { + "cellAddress": "C3", + "col": { + "col": "C", + "endOffset": 3, + "startOffset": 3, + }, + "endOffset": 4, + "ref": { + "col": 3, + "row": 3, + }, + "row": { + "endOffset": 4, + "row": 3, + "startOffset": 4, + }, + "startOffset": 3, + "type": "cell", + }, { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 6, + "startOffset": 6, + }, + "endOffset": 7, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 7, + "row": 1, + "startOffset": 7, + }, + "startOffset": 6, + "type": "cell", + }, { + "cellAddress": "C3", + "col": { + "col": "C", + "endOffset": 9, + "startOffset": 9, + }, + "endOffset": 10, + "ref": { + "col": 3, + "row": 3, + }, + "row": { + "endOffset": 10, + "row": 3, + "startOffset": 10, + }, + "startOffset": 9, + "type": "cell", + }, { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 12, + "startOffset": 12, + }, + "endOffset": 13, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 13, + "row": 1, + "startOffset": 13, + }, + "startOffset": 12, + "type": "cell", + }, { + "cellAddress": "C3", + "col": { + "col": "C", + "endOffset": 15, + "startOffset": 15, + }, + "endOffset": 16, + "ref": { + "col": 3, + "row": 3, + }, + "row": { + "endOffset": 16, + "row": 3, + "startOffset": 16, + }, + "startOffset": 15, + "type": "cell", + }, { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 18, + "startOffset": 18, + }, + "endOffset": 19, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 19, + "row": 1, + "startOffset": 19, + }, + "startOffset": 18, + "type": "cell", + }, { + "cellAddress": "B1", + "col": { + "col": "B", + "endOffset": 21, + "startOffset": 21, + }, + "endOffset": 22, + "ref": { + "col": 2, + "row": 1, + }, + "row": { + "endOffset": 22, + "row": 1, + "startOffset": 22, + }, + "startOffset": 21, + "type": "cell", + }]); + }); + + it('should parse ranges', () => { + let actual = refParser.parse('A1:C3', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 0, + "startOffset": 0, + }, + "endOffset": 1, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 1, + "row": 1, + "startOffset": 1, + }, + "startOffset": 0, + "type": "cell", + }, { + "cellAddress": "C3", + "col": { + "col": "C", + "endOffset": 3, + "startOffset": 3, + }, + "endOffset": 4, + "ref": { + "col": 3, + "row": 3, + }, + "row": { + "endOffset": 4, + "row": 3, + "startOffset": 4, + }, + "startOffset": 3, + "type": "cell", + }]); + actual = refParser.parse('A:C', position); + expect(actual).to.deep.eq([{ + "col": "A", + "endOffset": 0, + "ref": { + "col": 1, + "row": undefined, + }, + "startOffset": 0, + "type": "col", + }, { + "col": "C", + "endOffset": 2, + "ref": { + "col": 3, + "row": undefined, + }, + "startOffset": 2, + "type": "col", + }]); + actual = refParser.parse('1:3', position); + expect(actual).to.deep.eq([{ + "endOffset": 0, + "ref": { + "col": undefined, + "row": 1, + }, + "row": 1, + "startOffset": 0, + "type": "row", + }, { + "endOffset": 2, + "ref": { + "col": undefined, + "row": 3, + }, + "row": 3, + "startOffset": 2, + "type": "row", + }]); + }); + + it('should parse variable', function () { + let actual = refParser.parse('aaaa', position); + expect(actual).to.deep.eq([{ + "endOffset": 3, + "name": "aaaa", + "startOffset": 0, + "type": "variable", + }]); + }); + + it('should parse basic formulas', function () { + + // data types + let actual = refParser.parse('TRUE+A1+#VALUE!+{1}', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 5, + "startOffset": 5, + }, + "endOffset": 6, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 6, + "row": 1, + "startOffset": 6, + }, + "startOffset": 5, + "type": "cell", + }]); + + // function without args + actual = refParser.parse('A1+FUN()', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 0, + "startOffset": 0, + }, + "endOffset": 1, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 1, + "row": 1, + "startOffset": 1, + }, + "startOffset": 0, + "type": "cell", + }]); + + // prefix + actual = refParser.parse('++A1', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 2, + "startOffset": 2, + }, + "endOffset": 3, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 3, + "row": 1, + "startOffset": 3, + }, + "startOffset": 2, + "type": "cell", + }]); + + // postfix + actual = refParser.parse('A1%', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 0, + "startOffset": 0, + }, + "endOffset": 1, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 1, + "row": 1, + "startOffset": 1, + }, + "startOffset": 0, + "type": "cell", + }]); + + // intersect + actual = refParser.parse('A1:A3 A3:B3', position); + expect(actual).to.deep.eq([ { + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 0, + "startOffset": 0, + }, + "endOffset": 1, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 1, + "row": 1, + "startOffset": 1, + }, + "startOffset": 0, + "type": "cell", + }, { + "cellAddress": "A3", + "col": { + "col": "A", + "endOffset": 3, + "startOffset": 3, + }, + "endOffset": 4, + "ref": { + "col": 1, + "row": 3, + }, + "row": { + "endOffset": 4, + "row": 3, + "startOffset": 4, + }, + "startOffset": 3, + "type": "cell", + }, { + "cellAddress": "A3", + "col": { + "col": "A", + "endOffset": 6, + "startOffset": 6, + }, + "endOffset": 7, + "ref": { + "col": 1, + "row": 3, + }, + "row": { + "endOffset": 7, + "row": 3, + "startOffset": 7, + }, + "startOffset": 6, + "type": "cell", + }, { + "cellAddress": "B3", + "col": { + "col": "B", + "endOffset": 9, + "startOffset": 9, + }, + "endOffset": 10, + "ref": { + "col": 2, + "row": 3, + }, + "row": { + "endOffset": 10, + "row": 3, + "startOffset": 10, + }, + "startOffset": 9, + "type": "cell", + }]); + + // union + actual = refParser.parse('(A1:C1, A2:E9)', position); + expect(actual).to.deep.eq([{ + "cellAddress": "A1", + "col": { + "col": "A", + "endOffset": 1, + "startOffset": 1, + }, + "endOffset": 2, + "ref": { + "col": 1, + "row": 1, + }, + "row": { + "endOffset": 2, + "row": 1, + "startOffset": 2, + }, + "startOffset": 1, + "type": "cell", + }, { + "cellAddress": "C1", + "col": { + "col": "C", + "endOffset": 4, + "startOffset": 4, + }, + "endOffset": 5, + "ref": { + "col": 3, + "row": 1, + }, + "row": { + "endOffset": 5, + "row": 1, + "startOffset": 5, + }, + "startOffset": 4, + "type": "cell", + }, { + "cellAddress": "A2", + "col": { + "col": "A", + "endOffset": 8, + "startOffset": 8, + }, + "endOffset": 9, + "ref": { + "col": 1, + "row": 2, + }, + "row": { + "endOffset": 9, + "row": 2, + "startOffset": 9, + }, + "startOffset": 8, + "type": "cell", + }, { + "cellAddress": "E9", + "col": { + "col": "E", + "endOffset": 11, + "startOffset": 11, + }, + "endOffset": 12, + "ref": { + "col": 5, + "row": 9, + }, + "row": { + "endOffset": 12, + "row": 9, + "startOffset": 12, + }, + "startOffset": 11, + "type": "cell", + }]); + }); + + it('should parse complex formula', () => { + let actual = refParser.parse('IF(MONTH($K$1)<>MONTH($K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1)),"",$K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1))', position); + expect(actual).to.deep.eq([{ + "cellAddress": "$K$1", + "col": { + "col": "K", + "endOffset": 10, + "startOffset": 10, + }, + "endOffset": 12, + "ref": { + "col": 11, + "row": 1, + }, + "row": { + "endOffset": 12, + "row": 1, + "startOffset": 12, + }, + "startOffset": 9, + "type": "cell", + }, { + "cellAddress": "$K$1", + "col": { + "col": "K", + "endOffset": 23, + "startOffset": 23, + }, + "endOffset": 25, + "ref": { + "col": 11, + "row": 1, + }, + "row": { + "endOffset": 25, + "row": 1, + "startOffset": 25, + }, + "startOffset": 22, + "type": "cell", + }, { + "cellAddress": "$K$1", + "col": { + "col": "K", + "endOffset": 37, + "startOffset": 37, + }, + "endOffset": 39, + "ref": { + "col": 11, + "row": 1, + }, + "row": { + "endOffset": 39, + "row": 1, + "startOffset": 39, + }, + "startOffset": 36, + "type": "cell", + }, { + "endOffset": 53, + "name": "start_day", + "startOffset": 45, + "type": "variable", + }, { + "cellAddress": "$K$1", + "col": { + "col": "K", + "endOffset": 72, + "startOffset": 72, + }, + "endOffset": 74, + "ref": { + "col": 11, + "row": 1, + }, + "row": { + "endOffset": 74, + "row": 1, + "startOffset": 74, + }, + "startOffset": 71, + "type": "cell", + }, { + "endOffset": 88, + "name": "start_day", + "startOffset": 80, + "type": "variable", + }, { + "cellAddress": "O5", + "col": { + "col": "O", + "endOffset": 107, + "startOffset": 107, + }, + "endOffset": 108, + "ref": { + "col": 15, + "row": 5, + }, + "row": { + "endOffset": 108, + "row": 5, + "startOffset": 108, + }, + "startOffset": 107, + "type": "cell", + }, { + "cellAddress": "$K$3", + "col": { + "col": "K", + "endOffset": 116, + "startOffset": 116, + }, + "endOffset": 118, + "ref": { + "col": 11, + "row": 3, + }, + "row": { + "endOffset": 118, + "row": 3, + "startOffset": 118, + }, + "startOffset": 115, + "type": "cell", + }, { + "cellAddress": "O5", + "col": { + "col": "O", + "endOffset": 132, + "startOffset": 132, + }, + "endOffset": 133, + "ref": { + "col": 15, + "row": 5, + }, + "row": { + "endOffset": 133, + "row": 5, + "startOffset": 133, + }, + "startOffset": 132, + "type": "cell", + }, { + "cellAddress": "$K$3", + "col": { + "col": "K", + "endOffset": 144, + "startOffset": 144, + }, + "endOffset": 146, + "ref": { + "col": 11, + "row": 3, + }, + "row": { + "endOffset": 146, + "row": 3, + "startOffset": 146, + }, + "startOffset": 143, + "type": "cell", + }, { + "cellAddress": "$K$1", + "col": { + "col": "K", + "endOffset": 157, + "startOffset": 157, + }, + "endOffset": 159, + "ref": { + "col": 11, + "row": 1, + }, + "row": { + "endOffset": 159, + "row": 1, + "startOffset": 159, + }, + "startOffset": 156, + "type": "cell", + }, { + "cellAddress": "$K$1", + "col": { + "col": "K", + "endOffset": 171, + "startOffset": 171, + }, + "endOffset": 173, + "ref": { + "col": 11, + "row": 1, + }, + "row": { + "endOffset": 173, + "row": 1, + "startOffset": 173, + }, + "startOffset": 170, + "type": "cell", + }, { + "endOffset": 187, + "name": "start_day", + "startOffset": 179, + "type": "variable", + }, { + "cellAddress": "$K$1", + "col": { + "col": "K", + "endOffset": 206, + "startOffset": 206, + }, + "endOffset": 208, + "ref": { + "col": 11, + "row": 1, + }, + "row": { + "endOffset": 208, + "row": 1, + "startOffset": 208, + }, + "startOffset": 205, + "type": "cell", + }, { + "endOffset": 222, + "name": "start_day", + "startOffset": 214, + "type": "variable", + }, { + "cellAddress": "O5", + "col": { + "col": "O", + "endOffset": 241, + "startOffset": 241, + }, + "endOffset": 242, + "ref": { + "col": 15, + "row": 5, + }, + "row": { + "endOffset": 242, + "row": 5, + "startOffset": 242, + }, + "startOffset": 241, + "type": "cell", + }, { + "cellAddress": "$K$3", + "col": { + "col": "K", + "endOffset": 250, + "startOffset": 250, + }, + "endOffset": 252, + "ref": { + "col": 11, + "row": 3, + }, + "row": { + "endOffset": 252, + "row": 3, + "startOffset": 252, + }, + "startOffset": 249, + "type": "cell", + }, { + "cellAddress": "O5", + "col": { + "col": "O", + "endOffset": 266, + "startOffset": 266, + }, + "endOffset": 267, + "ref": { + "col": 15, + "row": 5, + }, + "row": { + "endOffset": 267, + "row": 5, + "startOffset": 267, + }, + "startOffset": 266, + "type": "cell", + }, { + "cellAddress": "$K$3", + "col": { + "col": "K", + "endOffset": 278, + "startOffset": 278, + }, + "endOffset": 280, + "ref": { + "col": 11, + "row": 3, + }, + "row": { + "endOffset": 280, + "row": 3, + "startOffset": 280, + }, + "startOffset": 277, + "type": "cell", + }]); + }); + + + const script_A_to_X_1_to_100 = [ { type: 'col', from: 1, to: 24, }, { type: 'row', from: 1, to: 100, } ]; + const script_A1_to_X100 = [ { type: 'cell', from: { col: 1, row: 1, }, to: { col: 24, row: 100, } } ]; + const script_aaaa_to_bbbbb = [ { type: 'variable', from: 'aaaa', to: 'bbbbb' } ]; + const script_start_day_to_first_day = [ { type: 'variable', from: 'start_day', to: 'first_day' } ]; + + it('should replace single cell', () => { + let actual = refParser.replace('A1', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100'); + actual = refParser.replace('A1', position, script_A1_to_X100); + expect(actual).to.eq('X100'); + actual = refParser.replace('A1+1', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100+1'); + actual = refParser.replace('A1+1', position, script_A1_to_X100); + expect(actual).to.eq('X100+1'); + }); + + it('should replace the same cell/range multiple times', () => { + let actual = refParser.replace('A1+A1+A1', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100+X100+X100'); + actual = refParser.replace('A1+A1+A1', position, script_A1_to_X100); + expect(actual).to.eq('X100+X100+X100'); + + actual = refParser.replace('A1:C3+A1:C3+A1:C3', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100:C3+X100:C3+X100:C3'); + actual = refParser.replace('A1:C3+A1:C3+A1:C3', position, script_A1_to_X100); + expect(actual).to.eq('X100:C3+X100:C3+X100:C3'); + + actual = refParser.replace('A1:C3+A1:C3+A1:C3+A1+B1', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100:C3+X100:C3+X100:C3+X100+B100'); + actual = refParser.replace('A1:C3+A1:C3+A1:C3+A1+B1', position, script_A1_to_X100); + expect(actual).to.eq('X100:C3+X100:C3+X100:C3+X100+B1'); + }); + + it('should replace ranges', () => { + let actual = refParser.replace('A1:C3', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100:C3'); + actual = refParser.replace('A1:C3', position, script_A1_to_X100); + expect(actual).to.eq('X100:C3'); + + actual = refParser.replace('A:C', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X:C'); + actual = refParser.replace('A:C', position, script_A1_to_X100); + expect(actual).to.eq('A:C'); + + actual = refParser.replace('1:3', position, script_A_to_X_1_to_100); + expect(actual).to.eq('100:3'); + actual = refParser.replace('1:3', position, script_A1_to_X100); + expect(actual).to.eq('1:3'); + }); + + it('should replace variable', function () { + let actual = refParser.replace('aaaa', position, script_aaaa_to_bbbbb); + expect(actual).to.eq('bbbbb'); + }); + + it('should replace basic formulas', function () { + + // data types + let actual = refParser.replace('TRUE+A1+#VALUE!+{1}', position, script_A_to_X_1_to_100); + expect(actual).to.eq('TRUE+X100+#VALUE!+{1}'); + actual = refParser.replace('TRUE+A1+#VALUE!+{1}', position, script_A1_to_X100); + expect(actual).to.eq('TRUE+X100+#VALUE!+{1}'); + + // function without args + actual = refParser.replace('A1+FUN()', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100+FUN()'); + actual = refParser.replace('A1+FUN()', position, script_A1_to_X100); + expect(actual).to.eq('X100+FUN()'); + + // prefix + actual = refParser.replace('++A1', position, script_A_to_X_1_to_100); + expect(actual).to.eq('++X100'); + actual = refParser.replace('++A1', position, script_A1_to_X100); + expect(actual).to.eq('++X100'); + + // postfix + actual = refParser.replace('A1%', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100%'); + actual = refParser.replace('A1%', position, script_A1_to_X100); + expect(actual).to.eq('X100%'); + + // intersect + actual = refParser.replace('A1:A3 A3:B3', position, script_A_to_X_1_to_100); + expect(actual).to.eq('X100:X3 X3:B3'); + actual = refParser.replace('A1:A3 A3:B3', position, script_A1_to_X100); + expect(actual).to.eq('X100:A3 A3:B3'); + + // union + actual = refParser.replace('(A1:C1, A2:E9)', position, script_A_to_X_1_to_100); + expect(actual).to.eq('(X100:C100, X2:E9)'); + actual = refParser.replace('(A1:C1, A2:E9)', position, script_A1_to_X100); + expect(actual).to.eq('(X100:C1, A2:E9)'); + }); + + it('should replace complex formula', () => { + let actual = refParser.replace('IF(MONTH($K$1)<>MONTH($K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1)),"",$K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1))', position, [...script_A_to_X_1_to_100, ...script_start_day_to_first_day]); + expect(actual).to.eq('IF(MONTH($K$100)<>MONTH($K$100-(WEEKDAY($K$100,1)-(first_day-1))-IF((WEEKDAY($K$100,1)-(first_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1)),"",$K$100-(WEEKDAY($K$100,1)-(first_day-1))-IF((WEEKDAY($K$100,1)-(first_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1))'); + actual = refParser.replace('IF(MONTH($K$1)<>MONTH($K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1)),"",$K$1-(WEEKDAY($K$1,1)-(start_day-1))-IF((WEEKDAY($K$1,1)-(start_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1))', position, [...script_A1_to_X100, ...script_start_day_to_first_day]); + expect(actual).to.eq('IF(MONTH($K$1)<>MONTH($K$1-(WEEKDAY($K$1,1)-(first_day-1))-IF((WEEKDAY($K$1,1)-(first_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1)),"",$K$1-(WEEKDAY($K$1,1)-(first_day-1))-IF((WEEKDAY($K$1,1)-(first_day-1))<=0,7,0)+(ROW(O5)-ROW($K$3))*7+(COLUMN(O5)-COLUMN($K$3)+1))'); + }); + + it('should exchange columns', function () { + let actual = refParser.replace('A1+AB28', position, [ { type: 'col', from: 1, to: 28, }, { type: 'col', from: 28, to: 1, } ]); + expect(actual).to.eq('AB1+A28'); + }); + + it('should exchange rows', function () { + let actual = refParser.replace('A1+AB28', position, [ { type: 'row', from: 1, to: 28, }, { type: 'row', from: 28, to: 1, } ]); + expect(actual).to.eq('A28+AB1'); + }); + + it('should exchange cells', function () { + let actual = refParser.replace('A1+AB28', position, [ { type: 'cell', from: { col: 1, row: 1, }, to: { col: 28, row: 28, } }, { type: 'cell', from: { col: 28, row: 28, }, to: { col: 1, row: 1, } } ]); + expect(actual).to.eq('AB28+A1'); + }); + + it('should exchange variables', function () { + let actual = refParser.replace('aaaa+bbbbb', position, [ { type: 'variable', from: 'aaaa', to: 'bbbbb' }, { type: 'variable', from: 'bbbbb', to: 'aaaa' } ]); + expect(actual).to.eq('bbbbb+aaaa'); + }); + + it('should invalidate column', function () { + let actual = refParser.replace('A1+AB28', position, [ { type: 'col', from: 1 } ]); + expect(actual).to.eq('#REF!+AB28'); + }); + + it('should invalidate row', function () { + let actual = refParser.replace('A1+AB28', position, [ { type: 'row', from: 1} ]); + expect(actual).to.eq('#REF!+AB28'); + }); + + it('should invalidate cell', function () { + let actual = refParser.replace('A1+AB28', position, [ { type: 'cell', from: { col: 1, row: 1, } } ]); + expect(actual).to.eq('#REF!+AB28'); + }); + + it('should invalidate variable', function () { + let actual = refParser.replace('aaaa+bbbbb', position, [ { type: 'variable', from: 'aaaa' } ]); + expect(actual).to.eq('#REF!+bbbbb'); + }); + + it('should not throw error', function () { + expect((() => refParser.parse('SUM(1))', position, true))) + .to.not.throw(); + + expect((() => refParser.parse('SUM(1+)', position, true))) + .to.not.throw(); + + expect((() => refParser.parse('SUM(1+)', position, true))) + .to.not.throw(); + }); +}); + diff --git a/test/test.js b/test/test.js index 00bf10bf..6986fa62 100644 --- a/test/test.js +++ b/test/test.js @@ -105,4 +105,5 @@ require('./grammar/test'); require('./grammar/errors'); require('./grammar/collection'); require('./grammar/depParser'); +require('./grammar/refParser'); require('./formulas');