Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/lesterlyu/fast-formula-parser

Parse and evaluate MS Excel formula in javascript.
https://github.com/lesterlyu/fast-formula-parser

evaluator excel excel-formula excel-formulas formula formula-parser interpreter javascript js parser spreadsheet

Last synced: 2 days ago
JSON representation

Parse and evaluate MS Excel formula in javascript.

Awesome Lists containing this project

README

        

### Announcement

This very excellent project is now being managed as part of

[![Logo](https://www.sheetxl.com/img/logo-color-text-135.png)](https://www.sheetxl.com)

---
![GitHub](https://img.shields.io/github/license/lesterlyu/fast-formula-parser)
[![npm (tag)](https://img.shields.io/npm/v/fast-formula-parser/latest)](https://www.npmjs.com/package/fast-formula-parser)
[![npm](https://img.shields.io/npm/dt/fast-formula-parser)](https://www.npmjs.com/package/fast-formula-parser)
[![Coverage Status](https://coveralls.io/repos/github/LesterLyu/fast-formula-parser/badge.svg?branch=master)](https://coveralls.io/github/LesterLyu/fast-formula-parser?branch=master)
## [A Fast Excel Formula Parser & Evaluator](https://github.com/LesterLyu/fast-formula-parser)

A fast and reliable excel formula parser in javascript. Using **LL(1)** parser.

### [Demo](https://lesterlyu.github.io/#/demo/fast-formula-parser)
### [Documentation](https://lesterlyu.github.io/fast-formula-parser/index.html)
### [Grammar Diagram](https://lesterlyu.github.io/fast-formula-parser/generated_diagrams.html)

### [SheetXL Home Page](https://www.sheetxl.com)
### Supports 280 Formulas
```
ABS, ACOS, ACOSH, ACOT, ACOTH, ADDRESS, AND, ARABIC, AREAS, ASC, ASIN, ASINH, ATAN, ATAN2, ATANH, AVEDEV, AVERAGE, AVERAGEA, AVERAGEIF, BAHTTEXT, BASE, BESSELI, BESSELJ, BESSELK, BESSELY, BETA.DIST, BETA.INV, BIN2DEC, BIN2HEX, BIN2OCT, BINOM.DIST, BINOM.DIST.RANGE, BINOM.INV, BITAND, BITLSHIFT, BITOR,
BITRSHIFT, BITXOR, CEILING, CEILING.MATH, CEILING.PRECISE, CHAR, CHISQ.DIST, CHISQ.DIST.RT, CHISQ.INV, CHISQ.INV.RT, CHISQ.TEST, CLEAN, CODE, COLUMN, COLUMNS, COMBIN, COMBINA, COMPLEX, CONCAT, CONCATENATE, CONFIDENCE.NORM, CONFIDENCE.T, CORREL, COS, COSH, COT, COTH, COUNT, COUNTIF, COVARIANCE.P,
COVARIANCE.S, CSC, CSCH, DATE, DATEDIF, DATEVALUE, DAY, DAYS, DAYS360, DBCS, DEC2BIN, DEC2HEX, DEC2OCT, DECIMAL, DEGREES, DELTA, DEVSQ, DOLLAR, EDATE, ENCODEURL, EOMONTH, ERF, ERFC, ERROR.TYPE, EVEN, EXACT, EXP, EXPON.DIST, F.DIST, F.DIST.RT, F.INV, F.INV.RT, F.TEST, FACT, FACTDOUBLE, FALSE, FIND, FINDB,
FISHER, FISHERINV, FIXED, FLOOR, FLOOR.MATH, FLOOR.PRECISE, FORECAST, FORECAST.LINEAR, FREQUENCY, GAMMA, GAMMA.DIST, GAMMA.INV, GAMMALN, GAMMALN.PRECISE, GAUSS, GCD, GEOMEAN, GESTEP, GROWTH, HARMEAN, HEX2BIN, HEX2DEC, HEX2OCT, HLOOKUP, HOUR, HYPGEOM.DIST, IF, IFERROR, IFNA, IFS, IMABS, IMAGINARY, IMARGUMENT,
IMCONJUGATE, IMCOS, IMCOSH, IMCOT, IMCSC, IMCSCH, IMDIV, IMEXP, IMLN, IMLOG10, IMLOG2, IMPOWER, IMPRODUCT, IMREAL, IMSEC, IMSECH, IMSIN, IMSINH, IMSQRT, IMSUB, IMSUM, IMTAN, INDEX, INT, INTERCEPT, ISBLANK, ISERR, ISERROR, ISEVEN, ISLOGICAL, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISOWEEKNUM, ISREF, ISTEXT,
KURT, LCM, LEFT, LEFTB, LN, LOG, LOG10, LOGNORM.DIST, LOGNORM.INV, LOWER, MDETERM, MID, MIDB, MINUTE, MMULT, MOD, MONTH, MROUND, MULTINOMIAL, MUNIT, N, NA, NEGBINOM.DIST, NETWORKDAYS, NETWORKDAYS.INTL, NORM.DIST, NORM.INV, NORM.S.DIST, NORM.S.INV, NOT, NOW, NUMBERVALUE, OCT2BIN, OCT2DEC, OCT2HEX, ODD, OR,
PHI, PI, POISSON.DIST, POWER, PRODUCT, PROPER, QUOTIENT, RADIANS, RAND, RANDBETWEEN, REPLACE, REPLACEB, REPT, RIGHT, RIGHTB, ROMAN, ROUND, ROUNDDOWN, ROUNDUP, ROW, ROWS, SEARCH, SEARCHB, SEC, SECH, SECOND, SERIESSUM, SIGN, SIN, SINH, SQRT, SQRTPI, STANDARDIZE, SUM, SUMIF, SUMPRODUCT, SUMSQ, SUMX2MY2,
SUMX2PY2, SUMXMY2, T, T.DIST, T.DIST.2T, T.DIST.RT, T.INV, T.INV.2T, TAN, TANH, TEXT, TIME, TIMEVALUE, TODAY, TRANSPOSE, TRIM, TRUE, TRUNC, TYPE, UNICHAR, UNICODE, UPPER, VLOOKUP, WEBSERVICE, WEEKDAY, WEEKNUM, WEIBULL.DIST, WORKDAY, WORKDAY.INTL, XOR, YEAR, YEARFRAC
```
### Size: 291KB Minified, 81KB Gzipped+Minified

### Background

Inspired by [XLParser](https://github.com/spreadsheetlab/XLParser/blob/master/src/XLParser/ExcelFormulaGrammar.cs)
and the paper ["A Grammar for Spreadsheet Formulas Evaluated on Two Large Datasets" by Efthimia Aivaloglou, David Hoepelman and Felienne Hermans](https://fenia266781730.files.wordpress.com/2019/01/07335408.pdf).

Note: The grammar in my implementation is different from theirs. My implementation gets rid of ambiguities to boost the performance.

### What is not supported:
- [External reference](https://support.office.com/en-ie/article/create-an-external-reference-link-to-a-cell-range-in-another-workbook-c98d1803-dd75-4668-ac6a-d7cca2a9b95f)
- Anything with `[` and `]`
- Ambiguous old styles
- Sheet name contains `:`, e.g. `SUM('1003:1856'!D6)`
- Sheet name with space that is not quoted, e.g. `I am a sheet!A1`
- `SUM(Sheet2:Sheet3!A1:C3)`
- You tell me

### Performance
- The expected performance is at least 3x faster than the optimized [formula-parser](https://github.com/LesterLyu/formula-parser).

### Dependency
- [Chevrotain](https://github.com/SAP/chevrotain) , thanks to this great parser building toolkit.

### [Examples](https://github.com/LesterLyu/fast-formula-parser/blob/master/examples/example.js)
- Install
```sh
npm i fast-formula-parser
# or using yarn
yarn add fast-formula-parser
```
- Import
```js
const FormulaParser = require('fast-formula-parser');
const {FormulaHelpers, Types, FormulaError, MAX_ROW, MAX_COLUMN} = FormulaParser;
// or
import FormulaParser, {FormulaHelpers, Types, FormulaError, MAX_ROW, MAX_COLUMN} from 'fast-formula-parser';
```
UMD minified build is also provides:
```html

```
- Basic Usage
```js
const data = [
// A B C
[1, 2, 3], // row 1
[4, 5, 6] // row 2
];

const parser = new FormulaParser({

// External functions, this will override internal functions with same name
functions: {
CHAR: (number) => {
number = FormulaHelpers.accept(number, Types.NUMBER);
if (number > 255 || number < 1)
throw FormulaError.VALUE;
return String.fromCharCode(number);
}
},

// Variable used in formulas (defined name)
// Should only return range reference or cell reference
onVariable: (name, sheetName) => {
// If it is a range reference (A1:B2)
return {
sheet: 'sheet name',
from: {
row: 1,
col: 1,
},
to: {
row: 2,
col: 2,
}
};
// If it is a cell reference (A1)
return {
sheet: 'sheet name',
row: 1,
col: 1
}
},

// retrieve cell value
onCell: ({sheet, row, col}) => {
// using 1-based index
// return the cell value, see possible types in next section.
return data[row - 1][col - 1];
},

// retrieve range values
onRange: (ref) => {
// using 1-based index
// Be careful when ref.to.col is MAX_COLUMN or ref.to.row is MAX_ROW, this will result in
// unnecessary loops in this approach.
const arr = [];
for (let row = ref.from.row; row <= ref.to.row; row++) {
const innerArr = [];
if (data[row - 1]) {
for (let col = ref.from.col; col <= ref.to.col; col++) {
innerArr.push(data[row - 1][col - 1]);
}
}
arr.push(innerArr);
}
return arr;
}
});

// position is required for evaluating certain formulas, e.g. ROW()
const position = {row: 1, col: 1, sheet: 'Sheet1'};

// parse the formula, the position of where the formula is located is required
// for some functions.
console.log(parser.parse('SUM(A:C)', position));
// print 21

// you can specify if the return value can be an array, this is helpful when dealing
// with an array formula
console.log(parser.parse('MMULT({1,5;2,3},{1,2;2,3})', position, true));
// print [ [ 11, 17 ], [ 8, 13 ] ]
```

- Custom Async functions
> Remember to use `await parser.parseAsync(...)` instead of `parser.parse(...)`
```js
const position = {row: 1, col: 1, sheet: 'Sheet1'};
const parser = new FormulaParser({
onCell: ref => {
return 1;
},
functions: {
DEMO_FUNC: async () => {
return [[1,2,3],[4,5,6]];
}
},
});
console.log(await parser.parseAsync('A1 + IMPORT_CSV())', position));
// print 2
console.log(await parser.parseAsync('SUM(DEMO_FUNC(), 1))', position));
// print 22
```
- Custom function requires parser context (e.g. location of the formula)
```js
const position = {row: 1, col: 1, sheet: 'Sheet1'};
const parser = new FormulaParser({
functionsNeedContext: {
// the first argument is the context
// the followings are the arguments passed to the function
ROW_PLUS_COL: (context, ...args) => {
return context.position.row + context.position.col;
}
},
});
console.log(await parser.parseAsync('SUM(ROW_PLUS_COL(), 1)', position));
// print 3
```

- Parse Formula Dependency
> This is helpful for building `dependency graph/tree`.
```js
import {DepParser} from 'fast-formula-parser';
const depParser = new DepParser({
// onVariable is the only thing you need provide if the formula contains variables
onVariable: variable => {
return 'VAR1' === variable ? {from: {row: 1, col: 1}, to: {row: 2, col: 2}} : {row: 1, col: 1};
}
});

// position of the formula should be provided
const position = {row: 1, col: 1, sheet: 'Sheet1'};

// Return an array of references (range reference or cell reference)
// This gives [{row: 1, col: 1, sheet: 'Sheet1'}]
depParser.parse('A1+1', position);

// This gives [{sheet: 'Sheet1', from: {row: 1, col: 1}, to: {row: 3, col: 3}}]
depParser.parse('A1:C3', position);

// This gives [{from: {row: 1, col: 1}, to: {row: 2, col: 2}}]
depParser.parse('VAR1 + 1', position);

// Complex formula
depParser.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);
// This gives the following result
const result = [
{
"col": 11,
"row": 1,
"sheet": "Sheet1",
},
{
"col": 1,
"row": 1,
"sheet": "Sheet1",
},
{
"col": 15,
"row": 5,
"sheet": "Sheet1",
},
{
"col": 11,
"row": 3,
"sheet": "Sheet1",
},
];
```

### 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`
- String: `'some string'`
- Boolean: `true`, `false`
- Array: `[[1, 2, true, 'str']]`
- Range Reference: (1-based index)
```js
const ref = {
sheet: String,
from: {
row: Number,
col: Number,
},
to: {
row: Number,
col: Number,
},
}
```
- Cell Reference: (1-based index)
```js
const ref = {
sheet: String,
row: Number,
col: Number,
}
```
- [Union (e.g. (A1:C3, E1:G6))](https://github.com/LesterLyu/fast-formula-parser/blob/master/grammar/type/collection.js)
- [FormulaError](https://lesterlyu.github.io/fast-formula-parser/FormulaError.html)
- `FormulaError.DIV0`: `#DIV/0!`
- `FormulaError.NA`: `#N/A`
- `FormulaError.NAME`: `#NAME?`
- `FormulaError.NULL`: `#NULL!`
- `FormulaError.NUM`: `#NUM!`
- `FormulaError.REF`: `#REF!`
- `FormulaError.VALUE`: `#VALUE!`

### Types Definition
> Comming soon

### Error handling

- Lexing/Parsing Error
> Error location is available at `error.details.errorLocation`
```js
try {
parser.parse('SUM(1))', position);
} catch (e) {
console.log(e);
// #ERROR!:
// SUM(1))
// ^
// Error at position 1:7
// Redundant input, expecting EOF but found: )

expect(e).to.be.instanceof(FormulaError);
expect(e.details.errorLocation.line).to.eq(1);
expect(e.details.errorLocation.column).to.eq(7);
expect(e.name).to.eq('#ERROR!');
expect(e.details.name).to.eq('NotAllInputParsedException');
}
```
- Error from internal/external functions or unexpected error from the parser
> The error will be wrapped into `FormulaError`. The exact error is in `error.details`.
```js
const parser = new FormulaParser({
functions: {
BAD_FN: () => {
throw new SyntaxError();
}
}
});

try {
parser.parse('SUM(1))', position);
} catch (e) {
expect(e).to.be.instanceof(FormulaError);
expect(e.name).to.eq('#ERROR!');
expect(e.details.name).to.eq('SyntaxError');
}
```

### Thanks
- [![JetBrains](https://raw.githubusercontent.com/LesterLyu/fast-formula-parser/master/logos/jetbrains-variant-4.svg)](https://www.jetbrains.com/?from=fast-formula-parser)