Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RefParser feature added to FormulaParser. #64

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
17 changes: 9 additions & 8 deletions grammar/parsing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {
Expand Down Expand Up @@ -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,
Expand Down
224 changes: 224 additions & 0 deletions grammar/references/hooks.js
Original file line number Diff line number Diff line change
@@ -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,
};
Loading