├── .vscode
├── settings.json
└── launch.json
├── .npmignore
├── .travis.yml
├── test
├── formulas
│ ├── text
│ │ ├── text.js
│ │ └── testcase.js
│ ├── index.js
│ ├── trigonometry
│ │ ├── trigonometry.js
│ │ └── testcase.js
│ ├── web
│ │ ├── testcase.js
│ │ └── web.js
│ ├── information
│ │ ├── information.js
│ │ └── testcase.js
│ ├── date
│ │ ├── date.js
│ │ └── testcase.js
│ ├── engineering
│ │ └── engineering.js
│ ├── logical
│ │ ├── logical.js
│ │ └── testcase.js
│ ├── reference
│ │ ├── reference.js
│ │ └── testcase.js
│ ├── math
│ │ ├── math.js
│ │ └── testcase.js
│ └── statistical
│ │ └── statistical.js
├── grammar
│ ├── collection.js
│ ├── errors.js
│ ├── test.js
│ └── depParser.js
├── utils.js
├── operators
│ ├── operators.js
│ └── testcase.js
└── test.js
├── examples
├── package.json
├── test.html
└── example.js
├── webpack.config.js
├── grammar
├── diagram.js
├── type
│ └── collection.js
├── dependency
│ ├── hooks.js
│ └── utils.js
├── lexing.js
├── utils.js
├── hooks.js
└── parsing.js
├── .github
└── workflows
│ └── nodejs.yml
├── index.js
├── .jsdoc.json
├── LICENSE
├── formulas
├── functions
│ ├── web.js
│ ├── financial.js
│ ├── logical.js
│ ├── information.js
│ ├── trigonometry.js
│ ├── statistical.js
│ ├── text.js
│ └── reference.js
├── error.js
└── operators.js
├── .gitignore
├── package.json
├── CODE_OF_CONDUCT.md
└── logos
└── jetbrains-variant-4.svg
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "sheetxl"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 | .idea
3 | .nyc_output
4 | .jsdoc.json
5 | .travis.yml
6 | greenkeeper.json
7 | coverage
8 | docs
9 | examples
10 | test
11 | xlsx
12 | logos
13 | CODE_OF_CONDUCT.md
14 | fast-formula-parser-*
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "stable"
4 | - "11"
5 | - "10"
6 | before_script:
7 | - export CI=true
8 | install:
9 | - npm install
10 | script:
11 | - npm test
12 | after_success:
13 | - npm run coverage:server
14 |
--------------------------------------------------------------------------------
/test/formulas/text/text.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 |
5 | const parser = new FormulaParser();
6 |
7 | describe('Text Functions', function () {
8 | generateTests(parser, TestCase);
9 | });
10 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "examples",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "example.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "fast-formula-parser": "latest"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test/formulas/index.js:
--------------------------------------------------------------------------------
1 | require('../operators/operators');
2 | require('./math/math');
3 | require('./trigonometry/trigonometry');
4 | require('./text/text');
5 | require('./reference/reference');
6 | require('./information/information');
7 | require('./statistical/statistical');
8 | require('./date/date');
9 | require('./engineering/engineering');
10 | require('./logical/logical');
11 | require('./web/web');
12 |
--------------------------------------------------------------------------------
/examples/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Formula Parser test
7 |
8 |
9 |
10 | Please open console and type FormulaParser
11 |
12 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
3 |
4 | module.exports = {
5 | mode: "production",
6 | entry: "./index.js",
7 | output: {
8 | path: path.resolve(__dirname, "./build/"),
9 | filename: "parser.min.js",
10 | library: "FormulaParser",
11 | libraryTarget: "umd",
12 | },
13 | plugins: [
14 | // new BundleAnalyzerPlugin(),
15 | ],
16 | };
17 |
--------------------------------------------------------------------------------
/grammar/diagram.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const chevrotain = require("chevrotain");
3 | const {Parser} = require("../grammar/parsing");
4 |
5 | // extract the serialized grammar.
6 | const parserInstance = new Parser();
7 | const serializedGrammar = parserInstance.getSerializedGastProductions();
8 |
9 | // create the HTML Text
10 | const htmlText = chevrotain.createSyntaxDiagramsCode(serializedGrammar);
11 |
12 | // Write the HTML file to disk
13 | fs.writeFileSync("./docs/generated_diagrams.html", htmlText);
14 |
--------------------------------------------------------------------------------
/test/formulas/trigonometry/trigonometry.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 | const data = [
5 | ['', 1,2,3,4],
6 | ['string', 3,4,5,6],
7 |
8 | ];
9 | const parser = new FormulaParser({
10 | onCell: ref => {
11 | return data[ref.row - 1][ref.col - 1];
12 | }
13 | });
14 |
15 | describe('Trigonometry Functions', function () {
16 | generateTests(parser, TestCase);
17 | });
18 |
--------------------------------------------------------------------------------
/test/formulas/web/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 | ENCODEURL: {
4 | 'ENCODEURL("http://contoso.sharepoint.com/teams/Finance/Documents/April Reports/Profit and Loss Statement.xlsx")':
5 | 'http%3A%2F%2Fcontoso.sharepoint.com%2Fteams%2FFinance%2FDocuments%2FApril%20Reports%2FProfit%20and%20Loss%20Statement.xlsx'
6 | },
7 | WEBSERVICE: {
8 | 'WEBSERVICE("www.google.ca")': FormulaError.ERROR()
9 | },
10 |
11 |
12 | };
13 |
--------------------------------------------------------------------------------
/test/formulas/information/information.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 | const data = [
5 | ['', 1,2,3,4],
6 | ['string', 3,4,5,6],
7 | [null, undefined]
8 |
9 | ];
10 | const parser = new FormulaParser({
11 | onCell: ref => {
12 | return data[ref.row - 1][ref.col - 1];
13 | }
14 | });
15 |
16 | describe('Information Functions', function () {
17 | generateTests(parser, TestCase);
18 | });
19 |
--------------------------------------------------------------------------------
/test/grammar/collection.js:
--------------------------------------------------------------------------------
1 | const expect = require('chai').expect;
2 | const Collection = require('../../grammar/type/collection');
3 |
4 | describe('Collection', () => {
5 | it('should throw error', function () {
6 | expect((() => new Collection([], [{row: 1, col: 1, sheet: 'Sheet1'}])))
7 | .to.throw('Collection: data length should match references length.')
8 | });
9 |
10 | it('should not throw error', function () {
11 | expect((() => new Collection([1], [{row: 1, col: 1, sheet: 'Sheet1'}])))
12 | .to.not.throw()
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [10.x, 12.x, 14.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v1
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - name: npm install, build, and test
21 | run: |
22 | npm install
23 | npm run build --if-present
24 | npm test
25 | env:
26 | CI: true
27 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('./grammar/hooks');
2 | const {DepParser} = require('./grammar/dependency/hooks');
3 | const SSF = require('./ssf/ssf');
4 | const FormulaError = require('./formulas/error');
5 |
6 | // const funs = new FormulaParser().supportedFunctions();
7 | // console.log('Supported:', funs.join(', '),
8 | // `\nTotal: ${funs.length}/477, ${funs.length/477*100}% implemented.`);
9 |
10 |
11 | Object.assign(FormulaParser, {
12 | MAX_ROW: 1048576,
13 | MAX_COLUMN: 16384,
14 | SSF,
15 | DepParser,
16 | FormulaError, ...require('./formulas/helpers')
17 | });
18 | module.exports = FormulaParser;
19 |
--------------------------------------------------------------------------------
/test/formulas/web/web.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 |
5 | const data = [
6 | ['fruit', 'price', 'count', 4, 5],
7 | ];
8 | const parser = new FormulaParser({
9 | onCell: ref => {
10 | return data[ref.row - 1][ref.col - 1];
11 | },
12 | onRange: ref => {
13 | const arr = [];
14 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
15 | const innerArr = [];
16 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
17 | innerArr.push(data[row][col])
18 | }
19 | arr.push(innerArr);
20 | }
21 | return arr;
22 | }
23 | });
24 |
25 | describe('Web Functions', function () {
26 | generateTests(parser, TestCase);
27 | });
28 |
--------------------------------------------------------------------------------
/.jsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tags": {
3 | "allowUnknownTags": true,
4 | "dictionaries": [
5 | "jsdoc",
6 | "closure"
7 | ]
8 | },
9 | "sourceType": "module",
10 | "source": {
11 | "include": [
12 | "formulas",
13 | "grammar",
14 | "ssf",
15 | "README.md",
16 | "docs/generated_diagrams.html"
17 | ],
18 | "includePattern": ".js$",
19 | "excludePattern": "(node_modules/|docs)"
20 | },
21 | "plugins": [
22 | "plugins/markdown"
23 | ],
24 | "markdown": {
25 | "idInHeadings": true
26 | },
27 | "templates": {
28 | "cleverLinks": false,
29 | "monospaceLinks": true,
30 | "useLongnameInNav": false,
31 | "showInheritedInNav": true
32 | },
33 | "opts": {
34 | "destination": "./docs/",
35 | "encoding": "utf8",
36 | "recurse": true,
37 | "verbose": true,
38 | "template": "./node_modules/docdash",
39 | "package": ""
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "skipFiles": [
12 | "/**"
13 | ],
14 | "program": "${workspaceFolder}\\index.js"
15 | }, {
16 | "name": "Mocha: Current File",
17 | "type": "node",
18 | "request": "launch",
19 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
20 | "args": [
21 | "-s 0",
22 | "--file",
23 | "${relativeFile}",
24 | "--no-timeout"
25 | ],
26 | "console": "integratedTerminal",
27 | "skipFiles": [
28 | "/**"
29 | ]
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/grammar/type/collection.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Represents unions.
3 | * (A1, A1:C5, ...)
4 | */
5 | class Collection {
6 |
7 | constructor(data, refs) {
8 | if (data == null && refs == null) {
9 | this._data = [];
10 | this._refs = [];
11 | } else {
12 | if (data.length !== refs.length)
13 | throw Error('Collection: data length should match references length.');
14 | this._data = data;
15 | this._refs = refs;
16 | }
17 | }
18 |
19 | get data() {
20 | return this._data;
21 | }
22 |
23 | get refs() {
24 | return this._refs;
25 | }
26 |
27 | get length() {
28 | return this._data.length;
29 | }
30 |
31 | /**
32 | * Add data and references to this collection.
33 | * @param {{}} obj - data
34 | * @param {{}} ref - reference
35 | */
36 | add(obj, ref) {
37 | this._data.push(obj);
38 | this._refs.push(ref);
39 | }
40 | }
41 |
42 | module.exports = Collection;
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Dishu(Lester) Lyu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | const expect = require('chai').expect;
2 | module.exports = {
3 | generateTests: (parser, TestCase) => {
4 | const funs = Object.keys(TestCase);
5 |
6 | funs.forEach(fun => {
7 | it(fun, () => {
8 | const formulas = Object.keys(TestCase[fun]);
9 | formulas.forEach(formula => {
10 | const expected = TestCase[fun][formula];
11 | let result = parser.parse(formula, {row: 1, col: 1});
12 | if (result.result) result = result.result;
13 | if (typeof result === "number" && typeof expected === "number") {
14 | expect(result, `${formula} should equal ${expected}\n`).to.closeTo(expected, 0.00000001);
15 | } else {
16 | // For FormulaError
17 | if (expected.equals)
18 | expect(expected.equals(result), `${formula} should equal ${expected.error}\n`).to.equal(true);
19 | else
20 | expect(result, `${formula} should equal ${expected}\n`).to.equal(expected);
21 | }
22 | })
23 | });
24 | });
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/test/formulas/date/date.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 |
5 | const data = [
6 | ['fruit', 'price', 'count', 4, 5],
7 | ['Apples', 0.69, 40, 5, 6],
8 | ['Bananas', 0.34, 38, 5, 6],
9 | [41235, 0.55, 15, 5, 6],
10 | [41247, 0.25, 25, 5, 6],
11 | [41295, 0.59, 40, 5, 6],
12 | ['Almonds', 2.80, 10, 5, 6], // row 7
13 | ['Cashews', 3.55, 16, 5, 6], // row 8
14 | ['Peanuts', 1.25, 20, 5, 6], // row 9
15 | ['Walnuts', 1.75, 12, 5, 6], // row 10
16 |
17 | ['Apples', 'Lemons',0, 0, 0], // row 11
18 | ['Bananas', 'Pears', 0, 0, 0], // row 12
19 | ];
20 | const parser = new FormulaParser({
21 | onCell: ref => {
22 | return data[ref.row - 1][ref.col - 1];
23 | },
24 | onRange: ref => {
25 | const arr = [];
26 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
27 | const innerArr = [];
28 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
29 | innerArr.push(data[row][col])
30 | }
31 | arr.push(innerArr);
32 | }
33 | return arr;
34 | }
35 | });
36 |
37 | describe('Date and Time Functions', function () {
38 | generateTests(parser, TestCase);
39 | });
40 |
--------------------------------------------------------------------------------
/test/formulas/engineering/engineering.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 |
5 | const data = [
6 | ['fruit', 'price', 'count', 4, 5],
7 | ['Apples', 0.69, 40, 5, 6],
8 | ['Bananas', 0.34, 38, 5, 6],
9 | [41235, 0.55, 15, 5, 6],
10 | [41247, 0.25, 25, 5, 6],
11 | [41295, 0.59, 40, 5, 6],
12 | ['Almonds', 2.80, 10, 5, 6], // row 7
13 | ['Cashews', 3.55, 16, 5, 6], // row 8
14 | ['Peanuts', 1.25, 20, 5, 6], // row 9
15 | ['Walnuts', 1.75, 12, 5, 6], // row 10
16 |
17 | ['Apples', 'Lemons',0, 0, 0], // row 11
18 | ['Bananas', 'Pears', 0, 0, 0], // row 12
19 | ];
20 | const parser = new FormulaParser({
21 | onCell: ref => {
22 | return data[ref.row - 1][ref.col - 1];
23 | },
24 | onRange: ref => {
25 | const arr = [];
26 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
27 | const innerArr = [];
28 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
29 | innerArr.push(data[row][col])
30 | }
31 | arr.push(innerArr);
32 | }
33 | return arr;
34 | }
35 | });
36 |
37 | describe('Engineering Functions', function () {
38 | generateTests(parser, TestCase);
39 | });
40 |
--------------------------------------------------------------------------------
/test/formulas/logical/logical.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 |
5 | const data = [
6 | ['fruit', 'price', 'count', 4, 5],
7 | ['Apples', 0.69, 40, 5, 6],
8 | ['Bananas', 0.34, 38, 5, 6],
9 | ['Lemons', 0.55, 15, 5, 6],
10 | ['Oranges', 0.25, 25, 5, 6],
11 | ['Pears', 0.59, 40, 5, 6],
12 | ['Almonds', 2.80, 10, 5, 6], // row 7
13 | ['Cashews', 3.55, 16, 5, 6], // row 8
14 | ['Peanuts', 1.25, 20, 5, 6], // row 9
15 | ['Walnuts', 1.75, 12, 5, 6], // row 10
16 |
17 | ['Apples', 'Lemons',0, 0, 0], // row 11
18 | ['Bananas', 'Pears', 0, 0, 0], // row 12
19 | ];
20 | const parser = new FormulaParser({
21 | onCell: ref => {
22 | return data[ref.row - 1][ref.col - 1];
23 | },
24 | onRange: ref => {
25 | const arr = [];
26 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
27 | const innerArr = [];
28 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
29 | innerArr.push(data[row][col])
30 | }
31 | arr.push(innerArr);
32 | }
33 | return arr;
34 | }
35 | });
36 |
37 | describe('Logical Functions', function () {
38 | generateTests(parser, TestCase);
39 | });
40 |
--------------------------------------------------------------------------------
/test/formulas/reference/reference.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 |
5 | const data = [
6 | ['fruit', 'price', 'count', 4, 5],
7 | ['Apples', 0.69, 40, 5, 6],
8 | ['Bananas', 0.34, 38, 5, 6],
9 | ['Lemons', 0.55, 15, 5, 6],
10 | ['Oranges', 0.25, 25, 5, 6],
11 | ['Pears', 0.59, 40, 5, 6],
12 | ['Almonds', 2.80, 10, 5, 6], // row 7
13 | ['Cashews', 3.55, 16, 5, 6], // row 8
14 | ['Peanuts', 1.25, 20, 5, 6], // row 9
15 | ['Walnuts', 1.75, 12, 5, 6], // row 10
16 |
17 | ['Apples', 'Lemons',0, 0, 0], // row 11
18 | ['Bananas', 'Pears', 0, 0, 0], // row 12
19 | ];
20 | const parser = new FormulaParser({
21 | onCell: ref => {
22 | return data[ref.row - 1][ref.col - 1];
23 | },
24 | onRange: ref => {
25 | const arr = [];
26 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
27 | const innerArr = [];
28 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
29 | innerArr.push(data[row][col])
30 | }
31 | arr.push(innerArr);
32 | }
33 | return arr;
34 | }
35 | });
36 |
37 | describe('Lookup and Reference Functions', function () {
38 | generateTests(parser, TestCase);
39 | });
40 |
--------------------------------------------------------------------------------
/formulas/functions/web.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers, Types} = require('../helpers');
3 | const H = FormulaHelpers;
4 |
5 | const WebFunctions = {
6 | ENCODEURL: text => {
7 | return encodeURIComponent(H.accept(text, Types.STRING));
8 | },
9 |
10 | FILTERXML: () => {
11 | // Not implemented due to extra dependency
12 | },
13 |
14 | WEBSERVICE: (context, url) => {
15 | throw FormulaError.ERROR('WEBSERVICE is not supported in sync mode.');
16 | if (typeof fetch === "function") {
17 | url = H.accept(url, Types.STRING);
18 | return fetch(url).then(res => res.text());
19 | } else {
20 | // Not implemented for Node.js due to extra dependency
21 | // Sample code for Node.js
22 | // const fetch = require('node-fetch');
23 | // url = H.accept(url, Types.STRING);
24 | // return fetch(url).then(res => res.text());
25 | throw FormulaError.ERROR('WEBSERVICE only available to browser with fetch.' +
26 | 'If you want to use WEBSERVICE in Node.js, please override this function: \n' +
27 | 'new FormulaParser({\n' +
28 | ' functionsNeedContext: {\n' +
29 | ' WEBSERVICE: (context, url) => {...}}\n' +
30 | '})');
31 | }
32 | }
33 | }
34 |
35 | module.exports = WebFunctions;
36 |
--------------------------------------------------------------------------------
/test/formulas/math/math.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 |
5 | const data = [
6 | [1, 2, 3, 4, 5],
7 | [100000, 7000, 250000, 5, 6],
8 | [200000, 14000, 4, 5, 6],
9 | [300000, 21000, 4, 5, 6],
10 | [400000, 28000, 4, 5, 6],
11 | ['string', 3, 4, 5, 6],
12 | // for SUMIF ex2
13 | ['Vegetables', 'Tomatoes', 2300, 5, 6], // row 7
14 | ['Vegetables', 'Celery', 5500, 5, 6], // row 8
15 | ['Fruits', 'Oranges', 800, 5, 6], // row 9
16 | ['', 'Butter', 400, 5, 6], // row 10
17 | ['Vegetables', 'Carrots', 4200, 5, 6], // row 11
18 | ['Fruits', 'Apples', 1200, 5, 6], // row 12
19 | ['1'],
20 | [2, 3, 9, 1, 8, 7, 5],
21 | [6, 5, 11, 7, 5, 4, 4],
22 | ];
23 | const parser = new FormulaParser({
24 | onCell: ref => {
25 | return data[ref.row - 1][ref.col - 1];
26 | },
27 | onRange: ref => {
28 | const arr = [];
29 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
30 | const innerArr = [];
31 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
32 | innerArr.push(data[row][col])
33 | }
34 | arr.push(innerArr);
35 | }
36 | return arr;
37 | }
38 | });
39 |
40 | describe('Math Functions', function () {
41 | generateTests(parser, TestCase);
42 | });
43 |
--------------------------------------------------------------------------------
/test/formulas/statistical/statistical.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../../utils');
4 | const data = [
5 | ['', true, 1, 'TRUE1', true],
6 | ['apples', 32, '{1,2}', 5, 6],
7 | ['oranges', 54, 4, 5, 6],
8 | ['peaches', 75, 4, 5, 6],
9 | ['apples', 86, 4, 5, 6],
10 | ['string', 3, 4, 5, 6],
11 | [1,2,3,4,5,6,7], // row 7
12 | [100000, 7000], //row 8
13 | [200000, 14000], //row 9
14 | [300000, 21000], //row 10
15 | [400000, 28000], //row 11
16 |
17 | ['East', 45678], //row 12
18 | ['West', 23789], //row 13
19 | ['North', -4789], //row 14
20 | ['South (New Office)', 0], //row 15
21 | ['MidWest', 9678], //row 16
22 |
23 | [undefined, true, 1, 2]
24 |
25 | ];
26 | const parser = new FormulaParser({
27 | onCell: ref => {
28 | return data[ref.row - 1][ref.col - 1];
29 | },
30 | onRange: ref => {
31 | const arr = [];
32 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
33 | const innerArr = [];
34 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
35 | innerArr.push(data[row][col])
36 | }
37 | arr.push(innerArr);
38 | }
39 | return arr;
40 | }
41 | });
42 |
43 | describe('Statistical Functions', function () {
44 | generateTests(parser, TestCase);
45 | });
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (https://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 | .env.test
64 |
65 | # parcel-bundler cache (https://parceljs.org/)
66 | .cache
67 |
68 | # next.js build output
69 | .next
70 |
71 | # nuxt.js build output
72 | .nuxt
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless/
79 |
80 | # FuseBox cache
81 | .fusebox/
82 |
83 | # DynamoDB Local files
84 | .dynamodb/
85 | package-lock.json
86 | .idea/
87 |
--------------------------------------------------------------------------------
/test/operators/operators.js:
--------------------------------------------------------------------------------
1 | const {FormulaParser} = require('../../grammar/hooks');
2 | const TestCase = require('./testcase');
3 | const {generateTests} = require('../utils');
4 | const FormulaError = require('../../formulas/error');
5 |
6 | const data = [
7 | [1, 2, 3, 4, 5],
8 | [100000, 7000, 250000, 5, 6],
9 | [200000, 14000, 4, 5, 6],
10 | [300000, 21000, 4, 5, 6],
11 | [400000, 28000, 4, 5, 6],
12 | ['string', 3, 4, 5, 6],
13 | // for SUMIF ex2
14 | ['Vegetables', 'Tomatoes', 2300, 5, 6], // row 7
15 | ['Vegetables', 'Celery', 5500, 5, 6], // row 8
16 | ['Fruits', 'Oranges', 800, 5, 6], // row 9
17 | ['', 'Butter', 400, 5, 6], // row 10
18 | ['Vegetables', 'Carrots', 4200, 5, 6], // row 11
19 | ['Fruits', 'Apples', 1200, 5, 6], // row 12
20 | [undefined, true, false, FormulaError.DIV0, 0] // row 13
21 | ];
22 |
23 | const parser = new FormulaParser({
24 | onCell: ref => {
25 | return data[ref.row - 1][ref.col - 1];
26 | },
27 | onRange: ref => {
28 | const arr = [];
29 | for (let row = ref.from.row - 1; row < ref.to.row; row++) {
30 | const innerArr = [];
31 | for (let col = ref.from.col - 1; col < ref.to.col; col++) {
32 | innerArr.push(data[row][col])
33 | }
34 | arr.push(innerArr);
35 | }
36 | return arr;
37 | },
38 | onVariable: name => {
39 | if (name === 'hello')
40 | return {row: 2, col: 2};
41 | else if (name === '_xlnm.print_titles')
42 | return {row: 7, col: 1};
43 | },
44 | });
45 |
46 | describe('Operators', function () {
47 | generateTests(parser, TestCase);
48 | });
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fast-formula-parser",
3 | "version": "1.0.19",
4 | "description": "fast excel formula parser",
5 | "repository": "https://github.com/LesterLyu/fast-formula-parser",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "mocha -s 0",
9 | "test:f": "mocha test/formulas",
10 | "prepublishOnly": "yarn run build && yarn run test",
11 | "postpublish": "yarn run publish:docs",
12 | "build": "webpack",
13 | "diagram": "node grammar/diagram.js",
14 | "docs": "yarn run diagram && jsdoc --configure .jsdoc.json --verbose",
15 | "publish:docs": "yarn run docs && yarn run coverage && gh-pages -d docs",
16 | "coverage": "nyc -x ssf -x test --reporter=html --reporter=text --report-dir=docs/coverage mocha",
17 | "coverage:f": "nyc -n \"formulas/functions/**\" -n \"formulas/operators.js\" --reporter=html --reporter=text mocha test/formulas",
18 | "coverage:server": "nyc -x ssf -x test --reporter=text-lcov --report-dir=docs/coverage mocha | coveralls"
19 | },
20 | "keywords": [
21 | "excel",
22 | "formula",
23 | "spreadsheet",
24 | "javascript",
25 | "js",
26 | "parser",
27 | "excel-formula"
28 | ],
29 | "author": "Lester(Dishu) Lyu",
30 | "license": "MIT",
31 | "dependencies": {
32 | "bahttext": "^1.1.0",
33 | "bessel": "^1.0.2",
34 | "chevrotain": "^7.0.1",
35 | "jstat": "^1.9.3"
36 | },
37 | "devDependencies": {
38 | "chai": "^4.2.0",
39 | "coveralls": "^3.1.0",
40 | "docdash": "^1.2.0",
41 | "gh-pages": "^3.1.0",
42 | "jsdoc": "^3.6.5",
43 | "mocha": "^7.2.0",
44 | "nyc": "^15.1.0",
45 | "webpack": "^4.44.0",
46 | "webpack-bundle-analyzer": "^3.8.0",
47 | "webpack-cli": "^3.3.12"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/test/formulas/logical/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 | AND: {
4 | 'AND(A1)': FormulaError.VALUE,
5 | 'AND(1,1,1)': true,
6 | 'AND(1,0,0)': false,
7 | 'AND(A2:C2)': true,
8 | 'AND("Test", "TRUE")': true,
9 | 'AND("Test", "FALSE")': false,
10 | 'AND({0,1,0}, FALSE)': false,
11 | 'AND((A2:C2, A3))': true,
12 | 'AND((A2:C2 C2))': true,
13 | },
14 |
15 | IF: {
16 | 'IF(TRUE, A1, A2)': 'fruit',
17 | 'IF(TRUE, A1&1, A2)': 'fruit1',
18 | 'IF(A1 = "fruit", A1, A2)': 'fruit',
19 | 'IF(IF(D1 < D5, A2) = "count", A1, A2)': 'Apples',
20 | },
21 |
22 | IFS: {
23 | 'IFS(1=3,"Not me", 1=2, "Me neither", 1=1, "Yes me")': 'Yes me',
24 | 'IFS(D5<60,"F",D5<70,"D",D5<80,"C",D5<90,"B",D5>=90,"A")': 'F',
25 | 'IFS(1=3,"Not me", 1=2, "Me neither", 1=4, "Not me")': FormulaError.NA,
26 | 'IFS("HELLO","Not me", 1=2, "Me neither", 1=4, "Not me")': FormulaError.VALUE,
27 | 'IFS("HELLO")': FormulaError.NA,
28 | },
29 |
30 | IFNA: {
31 | 'IFNA(#N/A, 1, 2)': FormulaError.NA,
32 | 'IFNA(#N/A, 1)': 1,
33 | 'IFNA("Good", 1)': 'Good'
34 | },
35 |
36 | OR: {
37 | 'OR(A1)': FormulaError.VALUE,
38 | 'OR(1,1,0)': true,
39 | 'OR(0,0,0)': false,
40 | 'OR(A2:C2)': true,
41 | 'OR("Test", "TRUE")': true,
42 | 'OR("Test", "FALSE")': false,
43 | 'OR({0,1,0}, FALSE)': true,
44 | 'OR((A2:C2, A3))': true,
45 | 'OR((A2:C2 C2))': true,
46 | },
47 |
48 | XOR: {
49 | 'XOR(A1)': FormulaError.VALUE,
50 | 'XOR(1,1,0)': false,
51 | 'XOR(1,1,1)': true,
52 | 'XOR(A2:C2)': false,
53 | 'XOR(A2:C2, "TRUE")': true,
54 | 'XOR("Test", "TRUE")': true,
55 | 'XOR({1,1,1}, FALSE)': true,
56 | 'XOR((A2:C2, A3))': false,
57 | 'XOR((A2:C2 C2))': true,
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/test/operators/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../formulas/error');
2 | module.exports = {
3 | unaryOp: {
4 | '+1': 1,
5 | '-1': -1,
6 | '--1': 1,
7 | '---1': -1,
8 | '+"A"': 'A',
9 | '+++"A"': 'A',
10 | '+++{"A"}': 'A',
11 | '++-+"A"': FormulaError.VALUE,
12 | '-"A"': FormulaError.VALUE,
13 | '+++{1,2,3}': 1,
14 | '+A6': 'string',
15 | '-A13': 0,
16 | '++{#VALUE!; 2; 3}': FormulaError.VALUE,
17 | },
18 |
19 | reference: {
20 | 'A1': 1,
21 | 'A1+2': 3,
22 | },
23 |
24 | compareOp: {
25 | '1>2': false,
26 | '1<2': true,
27 | '1=1': true,
28 | '1=2': false,
29 | '1<>1': false,
30 | '1<>2': true,
31 | '1>=2': false,
32 | '1<=2': true,
33 | '"a" & "b"': 'ab',
34 | '1&2': '12',
35 | '1<>"1"': true,
36 | '1TRUE': false,
38 | '1<=TRUE': true,
39 | '1>=TRUE': false,
40 | '"2">3': true,
41 | '#N/A>1': FormulaError.NA,
42 | 'A13>0': false,
43 | '0>A13': false,
44 | '{1;2;3} > 5': false,
45 | },
46 | concatOp: {
47 | '1&TRUE': '1TRUE',
48 | '1&FALSE': '1FALSE',
49 | 'TRUE&1': 'TRUE1',
50 | 'FALSE&1': 'FALSE1',
51 | '{1,2,3}&{TRUE;FALSE}': '1TRUE',
52 | '1REF!': FormulaError.REF,
53 | 'A13&"HELLO"': 'HELLO',
54 | '"HELLO"&A13': 'HELLO'
55 | },
56 | mathOp: {
57 | '1+A13': 1,
58 | 'A13+1': 1
59 | },
60 | 'Operator Precedence': {
61 | // '1+2*2': 5,
62 | '1+4/2+1': 4,
63 | '1+4/2+2*3': 9,
64 | '(1+4/2+2*3)/3^2': 1,
65 | '1-234/78+9/-78+45%': -1.6653846153846200
66 | },
67 | 'Intersection Test': {
68 | 'SUM(A1:C3 B2:D4)': 271004,
69 | 'SUM(A1:C3 C3:D4)': 4,
70 | 'SUM(A1:A1 A1:A1)': 1
71 | },
72 | Errors: {
73 | '#REF!': FormulaError.REF,
74 | '{#REF!}': FormulaError.REF
75 | },
76 | 'Defined Name': {
77 | 'hello + 1': 7001,
78 | '_xlnm.print_titles': 'Vegetables',
79 | },
80 |
81 | };
82 |
--------------------------------------------------------------------------------
/formulas/functions/financial.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers: H, Types} = require('../helpers');
3 | const {DATEVALUE, YEARFRAC} = require('./date');
4 |
5 | const FinancialFunctions = {
6 | /**
7 | * https://support.microsoft.com/en-us/office/accrint-function-fe45d089-6722-4fb3-9379-e1f911d8dc74
8 | */
9 | ACCRINT: (issue, firstInterest, settlement, rate, par, frequency, basis, calcMethod) => {
10 | issue = H.accept(issue);
11 | firstInterest = H.accept(firstInterest);
12 | settlement = H.accept(settlement);
13 |
14 | // Parse date string to serial
15 | if (typeof issue === "string") {
16 | firstInterest = DATEVALUE(firstInterest);
17 | }
18 | if (typeof issue === "string") {
19 | issue = DATEVALUE(issue);
20 | }
21 | if (typeof issue === "string") {
22 | settlement = DATEVALUE(settlement);
23 | }
24 |
25 | rate = H.accept(rate, Types.NUMBER);
26 | par = H.accept(par, Types.NUMBER);
27 | frequency = H.accept(frequency, Types.NUMBER);
28 | basis = H.accept(basis, Types.NUMBER, 0);
29 | calcMethod = H.accept(calcMethod, Types.BOOLEAN, true);
30 |
31 | // Issue, first_interest, settlement, frequency, and basis are truncated to integers
32 | issue = Math.trunc(issue);
33 | firstInterest = Math.trunc(firstInterest);
34 | settlement = Math.trunc(settlement);
35 | frequency = Math.trunc(frequency);
36 | basis = Math.trunc(basis);
37 |
38 | // If rate ≤ 0 or if par ≤ 0, ACCRINT returns the #NUM! error value.
39 | if (rate <= 0 || par <= 0 )
40 | return FormulaError.NUM;
41 |
42 | // If frequency is any number other than 1, 2, or 4, ACCRINT returns the #NUM! error value.
43 | if (frequency !== 1 && frequency !== 2 && frequency !== 4)
44 | return FormulaError.NUM;
45 |
46 | // If basis < 0 or if basis > 4, ACCRINT returns the #NUM! error value.
47 | if (basis < 0 || basis > 4)
48 | return FormulaError.NUM;
49 |
50 | // If issue ≥ settlement, ACCRINT returns the #NUM! error value.
51 | if (issue >= settlement)
52 | return FormulaError.NUM;
53 |
54 |
55 |
56 |
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/test/formulas/information/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 | 'ERROR.TYPE': {
4 | 'ERROR.TYPE(#NULL!)': 1,
5 | 'ERROR.TYPE(#DIV/0!)': 2,
6 | 'ERROR.TYPE(#N/A)': 7,
7 | 'ERROR.TYPE(#VALUE!)': 3,
8 | 'ERROR.TYPE(#REF!)': 4,
9 | 'ERROR.TYPE(#NUM!)': 6,
10 | 'ERROR.TYPE(#NAME?)': 5,
11 | },
12 |
13 | ISBLANK: {
14 | 'ISBLANK(A1)': true,
15 | 'ISBLANK(A2)': false,
16 | 'ISBLANK("")': false,
17 | 'ISBLANK(A3)': true,
18 | 'ISBLANK(B3)': true,
19 | 'ISBLANK({1})': false,
20 | },
21 |
22 | ISERR: {
23 | 'ISERR(1/0)': true,
24 | 'ISERR(#DIV/0!)': true,
25 | 'ISERR(#N/A)': false,
26 | },
27 |
28 | ISERROR: {
29 | 'ISERROR(1/0)': true,
30 | 'ISERROR(#DIV/0!)': true,
31 | 'ISERROR(#N/A)': true,
32 | 'ISERROR(#VALUE!)': true,
33 | 'ISERROR(#REF!)': true,
34 | 'ISERROR(#NUM!)': true,
35 | 'ISERROR(#NAME?)': true,
36 | },
37 |
38 | ISEVEN: {
39 | 'ISEVEN(2)': true,
40 | 'ISEVEN(-2)': true,
41 | 'ISEVEN(2.5)': true,
42 | 'ISEVEN(3)': false,
43 | },
44 |
45 | ISLOGICAL: {
46 | 'ISLOGICAL(TRUE)': true,
47 | 'ISLOGICAL(FALSE)': true,
48 | 'ISLOGICAL("TRUE")': false
49 | },
50 |
51 | ISNA: {
52 | 'ISNA(#N/A)': true,
53 | 'ISNA(#NAME?)': false,
54 | },
55 |
56 | ISNONTEXT: {
57 | 'ISNONTEXT(123)': true,
58 |
59 | },
60 |
61 | ISNUMBER: {
62 | 'ISNUMBER(123)': true,
63 | 'ISNUMBER(A1)': false,
64 | 'ISNUMBER(B1)': true,
65 | },
66 |
67 | ISREF: {
68 | 'ISREF(B2)': true,
69 | 'ISREF(123)': false,
70 | 'ISREF("A1")': false,
71 | 'ISREF(#REF!)': false,
72 | 'ISREF(XYZ1)': false,
73 | 'ISREF(A1:XYZ1)': false,
74 | 'ISREF(XYZ1:A1)': false
75 | },
76 |
77 | ISTEXT: {
78 | 'ISTEXT(123)': false,
79 | 'ISTEXT("123")': true,
80 | },
81 |
82 | N: {
83 | 'N(1)': 1,
84 | 'N(TRUE)': 1,
85 | 'N(FALSE)': 0,
86 | 'N(1/0)': FormulaError.DIV0,
87 | 'N("123")': 0,
88 | },
89 |
90 | NA: {
91 | 'NA()': FormulaError.NA,
92 | },
93 |
94 | TYPE: {
95 | // empty cell
96 | 'TYPE(A1)': 1,
97 | 'TYPE(12)': 1,
98 | 'TYPE("12")': 2,
99 | 'TYPE("")': 2,
100 | 'TYPE(TRUE)': 4,
101 | 'TYPE(1/0)': 16,
102 | 'TYPE({1;2;3})': 64,
103 | }
104 | };
105 |
--------------------------------------------------------------------------------
/examples/example.js:
--------------------------------------------------------------------------------
1 | // const FormulaParser = require('fast-formula-parser');
2 | const FormulaParser = require('../index');
3 | const {FormulaHelpers, Types, FormulaError, MAX_ROW, MAX_COLUMN} = FormulaParser;
4 |
5 | const data = [
6 | // A B C
7 | [1, 2, 3], // row 1
8 | [4, 5, 6] // row 2
9 | ];
10 |
11 | const parser = new FormulaParser({
12 |
13 | // External functions, this will override internal functions with same name
14 | functions: {
15 | CHAR: (number) => {
16 | number = FormulaHelpers.accept(number, Types.NUMBER);
17 | if (number > 255 || number < 1)
18 | throw FormulaError.VALUE;
19 | return String.fromCharCode(number);
20 | },
21 | },
22 |
23 | // Variable used in formulas (defined name)
24 | onVariable: (name, sheetName) => {
25 | // range reference (A1:B2)
26 | return {
27 | sheet: 'sheet name',
28 | from: {
29 | row: 1,
30 | col: 1,
31 | },
32 | to: {
33 | row: 2,
34 | col: 2,
35 | }
36 | };
37 | // cell reference (A1)
38 | return {
39 | sheet: 'sheet name',
40 | row: 1,
41 | col: 1
42 | }
43 | },
44 |
45 | // retrieve cell value
46 | onCell: ({sheet, row, col}) => {
47 | // using 1-based index
48 | // return the cell value, see possible types in next section.
49 | return data[row - 1][col - 1];
50 | },
51 |
52 | // retrieve range values
53 | onRange: (ref) => {
54 | // using 1-based index
55 | // Be careful when ref.to.col is MAX_COLUMN or ref.to.row is MAX_ROW, this will result in
56 | // unnecessary loops in this approach.
57 | const arr = [];
58 | for (let row = ref.from.row; row <= ref.to.row; row++) {
59 | const innerArr = [];
60 | if (data[row - 1]) {
61 | for (let col = ref.from.col; col <= ref.to.col; col++) {
62 | innerArr.push(data[row - 1][col - 1]);
63 | }
64 | }
65 | arr.push(innerArr);
66 | }
67 | return arr;
68 | }
69 | });
70 |
71 | // parse the formula, the position of where the formula is located is required
72 | // for some functions.
73 | console.log(parser.parse('SUM(A:C)', {sheet: 'Sheet 1', row: 1, col: 1}));
74 | // print 21
75 |
76 | // you can specify if the return value can be an array, this is helpful when dealing
77 | // with an array formula
78 | console.log(parser.parse('MMULT({1,5;2,3},{1,2;2,3})', {sheet: 'Sheet 1', row: 1, col: 1}, true));
79 | // print [ [ 11, 17 ], [ 8, 13 ] ]
80 |
81 | console.log(parser.parse('SUM(1, "3q")', {sheet: 'Sheet 1', row: 1, col: 1}));
82 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at lvds2000@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/formulas/functions/logical.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers, Types,} = require('../helpers');
3 | const H = FormulaHelpers;
4 |
5 | /**
6 | * Get the number of values that evaluate to true and false.
7 | * Cast Number and "TRUE", "FALSE" to boolean.
8 | * Ignore unrelated values.
9 | * @ignore
10 | * @param {any[]} params
11 | * @return {number[]}
12 | */
13 | function getNumLogicalValue(params) {
14 | let numTrue = 0, numFalse = 0;
15 | H.flattenParams(params, null, true, val => {
16 | const type = typeof val;
17 | if (type === "string") {
18 | if (val === 'TRUE')
19 | val = true;
20 | else if (val === 'FALSE')
21 | val = false;
22 | } else if (type === "number")
23 | val = Boolean(val);
24 |
25 | if (typeof val === "boolean") {
26 | if (val === true)
27 | numTrue++;
28 | else
29 | numFalse++;
30 | }
31 | });
32 | return [numTrue, numFalse];
33 | }
34 |
35 | const LogicalFunctions = {
36 | AND: (...params) => {
37 | const [numTrue, numFalse] = getNumLogicalValue(params);
38 |
39 | // OR returns #VALUE! if no logical values are found.
40 | if (numTrue === 0 && numFalse === 0)
41 | return FormulaError.VALUE;
42 |
43 | return numTrue > 0 && numFalse === 0;
44 | },
45 |
46 | FALSE: () => {
47 | return false;
48 | },
49 |
50 | // Special
51 | IF: (context, logicalTest, valueIfTrue, valueIfFalse) => {
52 | logicalTest = H.accept(logicalTest, Types.BOOLEAN);
53 | valueIfTrue = H.accept(valueIfTrue); // do not parse type
54 | valueIfFalse = H.accept(valueIfFalse, null, false); // do not parse type
55 |
56 | return logicalTest ? valueIfTrue : valueIfFalse;
57 | },
58 |
59 | IFERROR: (value, valueIfError) => {
60 | return value.value instanceof FormulaError ? H.accept(valueIfError) : H.accept(value);
61 | },
62 |
63 | IFNA: function (value, valueIfNa) {
64 | if (arguments.length > 2)
65 | throw FormulaError.TOO_MANY_ARGS('IFNA');
66 | return FormulaError.NA.equals(value.value) ? H.accept(valueIfNa) : H.accept(value);
67 | },
68 |
69 | IFS: (...params) => {
70 | if (params.length % 2 !== 0)
71 | return new FormulaError('#N/A', 'IFS expects all arguments after position 0 to be in pairs.');
72 |
73 | for (let i = 0; i < params.length / 2; i++) {
74 | const logicalTest = H.accept(params[i * 2], Types.BOOLEAN);
75 | const valueIfTrue = H.accept(params[i * 2 + 1]);
76 | if (logicalTest)
77 | return valueIfTrue;
78 | }
79 |
80 | return FormulaError.NA;
81 | },
82 |
83 | NOT: (logical) => {
84 | logical = H.accept(logical, Types.BOOLEAN);
85 | return !logical;
86 | },
87 |
88 | OR: (...params) => {
89 | const [numTrue, numFalse] = getNumLogicalValue(params);
90 |
91 | // OR returns #VALUE! if no logical values are found.
92 | if (numTrue === 0 && numFalse === 0)
93 | return FormulaError.VALUE;
94 |
95 | return numTrue > 0;
96 | },
97 |
98 | SWITCH: (...params) => {
99 |
100 | },
101 |
102 | TRUE: () => {
103 | return true;
104 | },
105 |
106 | XOR: (...params) => {
107 | const [numTrue, numFalse] = getNumLogicalValue(params);
108 |
109 | // XOR returns #VALUE! if no logical values are found.
110 | if (numTrue === 0 && numFalse === 0)
111 | return FormulaError.VALUE;
112 |
113 | return numTrue % 2 === 1;
114 | },
115 | };
116 |
117 | module.exports = LogicalFunctions;
118 |
--------------------------------------------------------------------------------
/formulas/error.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Formula Error.
3 | */
4 | class FormulaError extends Error {
5 |
6 | /**
7 | * @param {string} error - error code, i.e. #NUM!
8 | * @param {string} [msg] - detailed error message
9 | * @param {object|Error} [details]
10 | * @returns {FormulaError}
11 | */
12 | constructor(error, msg, details) {
13 | super(msg);
14 | if (msg == null && details == null && FormulaError.errorMap.has(error))
15 | return FormulaError.errorMap.get(error);
16 | else if (msg == null && details == null) {
17 | this._error = error;
18 | FormulaError.errorMap.set(error, this);
19 | } else {
20 | this._error = error;
21 | }
22 | this.details = details;
23 | }
24 |
25 | /**
26 | * Get the error name.
27 | * @returns {string} formula error
28 | */
29 | get error() {
30 | return this._error;
31 | }
32 | get name() {
33 | return this._error;
34 | }
35 |
36 | /**
37 | * Return true if two errors are same.
38 | * @param {FormulaError} err
39 | * @returns {boolean} if two errors are same.
40 | */
41 | equals(err) {
42 | return err instanceof FormulaError && err._error === this._error;
43 | }
44 |
45 | /**
46 | * Return the formula error in string representation.
47 | * @returns {string} the formula error in string representation.
48 | */
49 | toString() {
50 | return this._error;
51 | }
52 | }
53 |
54 | FormulaError.errorMap = new Map();
55 |
56 | /**
57 | * DIV0 error
58 | * @type {FormulaError}
59 | */
60 | FormulaError.DIV0 = new FormulaError("#DIV/0!");
61 |
62 | /**
63 | * NA error
64 | * @type {FormulaError}
65 | */
66 | FormulaError.NA = new FormulaError("#N/A");
67 |
68 | /**
69 | * NAME error
70 | * @type {FormulaError}
71 | */
72 | FormulaError.NAME = new FormulaError("#NAME?");
73 |
74 | /**
75 | * NULL error
76 | * @type {FormulaError}
77 | */
78 | FormulaError.NULL = new FormulaError("#NULL!");
79 |
80 | /**
81 | * NUM error
82 | * @type {FormulaError}
83 | */
84 | FormulaError.NUM = new FormulaError("#NUM!");
85 |
86 | /**
87 | * REF error
88 | * @type {FormulaError}
89 | */
90 | FormulaError.REF = new FormulaError("#REF!");
91 |
92 | /**
93 | * VALUE error
94 | * @type {FormulaError}
95 | */
96 | FormulaError.VALUE = new FormulaError("#VALUE!");
97 |
98 | /**
99 | * NOT_IMPLEMENTED error
100 | * @param {string} functionName - the name of the not implemented function
101 | * @returns {FormulaError}
102 | * @constructor
103 | */
104 | FormulaError.NOT_IMPLEMENTED = (functionName) => {
105 | return new FormulaError("#NAME?", `Function ${functionName} is not implemented.`)
106 | };
107 |
108 | /**
109 | * TOO_MANY_ARGS error
110 | * @param functionName - the name of the errored function
111 | * @returns {FormulaError}
112 | * @constructor
113 | */
114 | FormulaError.TOO_MANY_ARGS = (functionName) => {
115 | return new FormulaError("#N/A", `Function ${functionName} has too many arguments.`)
116 | };
117 |
118 | /**
119 | * ARG_MISSING error
120 | * @param args - the name of the errored function
121 | * @returns {FormulaError}
122 | * @constructor
123 | */
124 | FormulaError.ARG_MISSING = (args) => {
125 | const {Types} = require('./helpers');
126 | return new FormulaError("#N/A", `Argument type ${args.map(arg => Types[arg]).join(', ')} is missing.`)
127 | };
128 |
129 | /**
130 | * #ERROR!
131 | * Parse/Lex error or other unexpected errors
132 | * @param {string} msg
133 | * @param {object|Error} [details]
134 | * @return {FormulaError}
135 | * @constructor
136 | */
137 | FormulaError.ERROR = (msg, details) => {
138 | return new FormulaError('#ERROR!', msg, details);
139 | }
140 |
141 | module.exports = FormulaError;
142 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const expect = require('chai').expect;
3 | const {FormulaParser} = require('../grammar/hooks');
4 | const parser = new FormulaParser(undefined, true);
5 |
6 | const fs = require('fs');
7 |
8 | const lineReader = require('readline').createInterface({
9 | input: fs.createReadStream('./test/formulas.txt')
10 | });
11 |
12 |
13 | describe('Parsing Formulas 1', function () {
14 | let success = 0;
15 | const formulas = [];
16 | const failures = [];
17 | before((done) => {
18 | lineReader.on('line', (line) => {
19 | line = line.slice(1, -1)
20 | .replace(/""/g, '"');
21 | if (line.indexOf('[') === -1)
22 | formulas.push(line);
23 | // else
24 | // console.log(`not supported: ${line}`)
25 | // console.log(line)
26 | });
27 | lineReader.on('close', () => {
28 | done();
29 | });
30 |
31 | });
32 |
33 | it('formulas parse rate should be 100%', function () {
34 | this.timeout(20000);
35 | // console.log(formulas.length);
36 | formulas.forEach((formula, index) => {
37 | // console.log('testing #', index, formula);
38 | try {
39 | parser.parse(formula, {row: 2, col: 2});
40 | success++;
41 | } catch (e) {
42 | failures.push(formula);
43 | }
44 | });
45 | if (failures.length > 0) {
46 | console.log(failures);
47 | console.log(`Success rate: ${success / formulas.length * 100}%`);
48 | }
49 |
50 | const logs = parser.logs.sort();
51 | console.log(`The following functions is not implemented: (${logs.length} in total)\n ${logs.join(', ')}`);
52 | parser.logs = [];
53 | assert.strictEqual(success / formulas.length === 1, true);
54 | });
55 | });
56 |
57 | describe('Parsing Formulas 2', () => {
58 | let success = 0;
59 | let formulas;
60 | const failures = [];
61 | before(done => {
62 | fs.readFile('./test/formulas2.json', (err, data) => {
63 | if (err) throw err;
64 | formulas = JSON.parse(data.toString());
65 | done();
66 | });
67 | });
68 |
69 | it ('skip', () => '');
70 |
71 | it('custom formulas parse rate should be 100%', function(done) {
72 | this.timeout(20000);
73 | formulas.forEach((formula, index) => {
74 | // console.log('testing #', index, formula);
75 | try {
76 | parser.parse(formula);
77 | success++;
78 | } catch (e) {
79 | failures.push(formula);
80 | }
81 | });
82 | if (failures.length > 0) {
83 | console.log(failures);
84 | console.log(`Success rate: ${success / formulas.length * 100}%`);
85 | }
86 | assert.strictEqual(success / formulas.length === 1, true);
87 | const logs = parser.logs.sort();
88 | console.log(`The following functions is not implemented: (${logs.length} in total)\n ${logs.join(', ')}`);
89 | parser.logs = [];
90 | done();
91 | });
92 |
93 | });
94 |
95 | describe('Get supported formulas', () => {
96 | const functionsNames = parser.supportedFunctions();
97 | expect(functionsNames.length).to.greaterThan(275);
98 | console.log(`Support ${functionsNames.length} functions:\n${functionsNames.join(', ')}`);
99 | expect(functionsNames.includes('IFNA')).to.eq(true);
100 | expect(functionsNames.includes('SUMIF')).to.eq(true);
101 |
102 | })
103 |
104 | require('./grammar/test');
105 | require('./grammar/errors');
106 | require('./grammar/collection');
107 | require('./grammar/depParser');
108 | require('./formulas');
109 |
--------------------------------------------------------------------------------
/test/formulas/trigonometry/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 | ACOS: {
4 | 'ACOS(-0.5)': 2.094395102,
5 | 'ACOS(-0.5)*180/PI()': 120,
6 | 'DEGREES(ACOS(-0.5))': 120,
7 | 'ACOS(-1.5)': FormulaError.NUM,
8 | },
9 |
10 | ACOSH: {
11 | 'ACOSH(1)': 0,
12 | 'ACOSH(10)': 2.993222846,
13 | 'ACOSH(0.99)': FormulaError.NUM,
14 | },
15 |
16 | ACOT: {
17 | 'ACOT(2)': 0.463647609,
18 | 'ACOT(-2)': 2.677945045,
19 | },
20 |
21 | ACOTH: {
22 | 'ACOTH(-5)': -0.202732554,
23 | 'ACOTH(6)': 0.168236118,
24 | 'ACOTH(0.99)': FormulaError.NUM,
25 | },
26 |
27 | ASIN: {
28 | 'ASIN(-0.5)': -0.523598776,
29 | 'ASIN(-0.5)*180/PI()': -30,
30 | 'DEGREES(ASIN(-0.5))': -30,
31 | 'ASIN(-1.5)': FormulaError.NUM,
32 | },
33 |
34 | ASINH: {
35 | 'ASINH(-2.5)': -1.647231146,
36 | 'ASINH(10)': 2.99822295,
37 | },
38 |
39 | ATAN: {
40 | 'ATAN(0)': 0,
41 | 'ATAN(1)': 0.785398163,
42 | 'ATAN(1)*180/PI()': 45,
43 | 'DEGREES(ATAN(1))': 45,
44 | },
45 |
46 | ATAN2: {
47 | 'ATAN2(1, 1)': 0.785398163,
48 | 'ATAN2(-1, -1)': -2.35619449,
49 | 'ATAN2(-1, -1)*180/PI()': -135,
50 | 'DEGREES(ATAN2(-1, -1))': -135,
51 | 'ATAN2(0,0)': FormulaError.DIV0
52 | },
53 |
54 | ATANH: {
55 | 'ATANH(0.76159416)': 1.00000001,
56 | 'ATANH(-0.1)': -0.100335348,
57 | 'ATANH(-1.1)': FormulaError.NUM,
58 | },
59 |
60 | COS: {
61 | 'COS(1.047)': 0.500171075,
62 | 'COS(60*PI()/180)': 0.5,
63 | 'COS(RADIANS(60))': 0.5,
64 | 'COS(2^27-1)': -0.293388404,
65 | 'COS(2^27)': FormulaError.NUM,
66 | },
67 |
68 | COSH: {
69 | 'COSH(4)': 27.30823284,
70 | 'COSH(EXP(1))': 7.610125139,
71 | 'COSH(0)': 1,
72 | 'COSH(800)': FormulaError.NUM, // infinity
73 | },
74 |
75 | COT: {
76 | 'COT(30)': -0.156119952,
77 | 'COT(45)': 0.617369624,
78 | 'COT(2^27-1)': 0.306893777,
79 | 'COT(2^27)': FormulaError.NUM,
80 | 'COT(0)': FormulaError.DIV0,
81 | },
82 |
83 | COTH: {
84 | 'COTH(2)': 1.037314721,
85 | 'COTH(0)': FormulaError.DIV0,
86 | 'COTH(2^100)': 1, // no value error here
87 | 'COTH(-2^100)': 1,
88 | },
89 |
90 | CSC: {
91 | 'CSC(15)': 1.537780562,
92 | 'CSC(2^27-1)': -1.046032404,
93 | 'CSC(2^27)': FormulaError.NUM,
94 | },
95 |
96 | CSCH: {
97 | 'CSCH(1.5)': 0.469642441,
98 | 'CSCH(2^100)': 0,
99 | 'CSCH(-2^100)': 0,
100 | 'CSCH(0)': FormulaError.DIV0,
101 | },
102 |
103 | SEC: {
104 | 'SEC(45)': 1.903594407,
105 | 'SEC(2^27-1)': -3.408451009,
106 | 'SEC(2^27)': FormulaError.NUM,
107 | },
108 |
109 | SECH: {
110 | 'SECH(45)': 5.7250371611E-20,
111 | 'SECH(2^100)': 0,
112 | 'SECH(-2^100)': 0,
113 | 'SECH(0)': 1,
114 | },
115 |
116 | SIN: {
117 | 'SIN(PI())': 0,
118 | 'SIN(PI()/2)': 1,
119 | 'SIN(30*PI()/180)': 0.5,
120 | 'SIN(RADIANS(30))': 0.5,
121 | 'SIN(2^27-1)': -0.955993329,
122 | 'SIN(2^27)': FormulaError.NUM,
123 | },
124 |
125 | SINH: {
126 | '2.868*SINH(0.0342*1.03)': 0.101049063,
127 | 'SINH(800)': FormulaError.NUM,
128 | },
129 |
130 | TAN: {
131 | 'TAN(0.785)': 0.99920399,
132 | 'TAN(45*PI()/180)': 1,
133 | 'TAN(RADIANS(45))': 1,
134 | 'TAN(0)': 0,
135 | 'TAN(PI())': -1.22515E-16,
136 | 'TAN(2^27-1)': 3.258456426,
137 | 'TAN(2^27)': FormulaError.NUM,
138 | },
139 |
140 | TANH: {
141 | 'TANH(-2)': -0.96402758,
142 | 'TANH(0)': 0,
143 | 'TANH(0.5)': 0.462117157,
144 | 'TANH(2^100)': 1,
145 | 'TANH(-2^100)': 1,
146 | },
147 | };
148 |
--------------------------------------------------------------------------------
/formulas/functions/information.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers, Types} = require('../helpers');
3 | const H = FormulaHelpers;
4 |
5 | const error2Number = {
6 | '#NULL!': 1, '#DIV/0!': 2, '#VALUE!': 3, '#REF!': 4, '#NAME?': 5,
7 | '#NUM!': 6, '#N/A': 7
8 | };
9 |
10 | const InfoFunctions = {
11 |
12 | CELL: (infoType, reference) => {
13 | // throw FormulaError.NOT_IMPLEMENTED('CELL');
14 | },
15 |
16 | 'ERROR.TYPE': (value) => {
17 | value = H.accept(value);
18 | if ( value instanceof FormulaError)
19 | return error2Number[value.toString()];
20 | throw FormulaError.NA;
21 | },
22 |
23 | INFO: () => {
24 | },
25 |
26 | ISBLANK: (value) => {
27 | if (!value.ref)
28 | return false;
29 | // null and undefined are also blank
30 | return value.value == null || value.value === '';
31 | },
32 |
33 | ISERR: (value) => {
34 | value = H.accept(value);
35 | return value instanceof FormulaError && value.toString() !== '#N/A';
36 | },
37 |
38 | ISERROR: (value) => {
39 | value = H.accept(value);
40 | return value instanceof FormulaError;
41 | },
42 |
43 | ISEVEN: number => {
44 | number = H.accept(number, Types.NUMBER);
45 | number = Math.trunc(number);
46 | return number % 2 === 0;
47 | },
48 |
49 | ISLOGICAL: (value) => {
50 | value = H.accept(value);
51 | return typeof value === 'boolean';
52 | },
53 |
54 | ISNA: (value) => {
55 | value = H.accept(value);
56 | return value instanceof FormulaError && value.toString() === '#N/A';
57 | },
58 |
59 | ISNONTEXT: (value) => {
60 | value = H.accept(value);
61 | return typeof value !== 'string';
62 | },
63 |
64 | ISNUMBER: (value) => {
65 | value = H.accept(value);
66 | return typeof value === "number";
67 | },
68 |
69 | ISREF: (value) => {
70 | if (!value.ref)
71 | return false;
72 | if (H.isCellRef(value) && (value.ref.row > 1048576 || value.ref.col > 16384)) {
73 | return false;
74 | }
75 | if (H.isRangeRef(value) && (value.ref.from.row > 1048576 || value.ref.from.col > 16384
76 | || value.ref.to.row > 1048576 || value.ref.to.col > 16384)) {
77 | return false;
78 | }
79 | value = H.accept(value);
80 | return !(value instanceof FormulaError && value.toString() === '#REF!');
81 | },
82 |
83 | ISTEXT: (value) => {
84 | value = H.accept(value);
85 | return typeof value === 'string';
86 | },
87 |
88 | N: value => {
89 | value = H.accept(value);
90 | const type = typeof value;
91 | if (type === 'number')
92 | return value;
93 | else if (type === "boolean")
94 | return Number(value);
95 | else if (value instanceof FormulaError)
96 | throw value;
97 | return 0;
98 | },
99 |
100 | NA: () => {
101 | throw FormulaError.NA;
102 | },
103 |
104 | TYPE: value => {
105 | // a reference
106 | if (value.ref) {
107 | if (H.isRangeRef(value)) {
108 | return 16;
109 | } else if (H.isCellRef(value)) {
110 | value = H.accept(value);
111 | // empty cell is number type
112 | if (typeof value === "string" && value.length === 0)
113 | return 1;
114 | }
115 | }
116 | value = H.accept(value);
117 | const type = typeof value;
118 | if (type === 'number')
119 | return 1;
120 | else if (type === "string")
121 | return 2;
122 | else if (type === "boolean")
123 | return 4;
124 | else if (value instanceof FormulaError)
125 | return 16;
126 | else if (Array.isArray(value))
127 | return 64;
128 | },
129 | };
130 |
131 |
132 | module.exports = InfoFunctions;
133 |
--------------------------------------------------------------------------------
/formulas/functions/trigonometry.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers, Types} = require('../helpers');
3 | const H = FormulaHelpers;
4 | const MAX_NUMBER = 2 ** 27 - 1;
5 |
6 | // https://support.office.com/en-us/article/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb
7 | const TrigFunctions = {
8 | ACOS: number => {
9 | number = H.accept(number, Types.NUMBER);
10 | if (number > 1 || number < -1)
11 | throw FormulaError.NUM;
12 | return Math.acos(number);
13 | },
14 |
15 | ACOSH: number => {
16 | number = H.accept(number, Types.NUMBER);
17 | if (number < 1)
18 | throw FormulaError.NUM;
19 | return Math.acosh(number);
20 | },
21 |
22 | ACOT: number => {
23 | number = H.accept(number, Types.NUMBER);
24 | return Math.PI / 2 - Math.atan(number);
25 | },
26 |
27 | ACOTH: number => {
28 | number = H.accept(number, Types.NUMBER);
29 | if (Math.abs(number) <= 1)
30 | throw FormulaError.NUM;
31 | return Math.atanh(1 / number);
32 | },
33 |
34 | ASIN: number => {
35 | number = H.accept(number, Types.NUMBER);
36 | if (number > 1 || number < -1)
37 | throw FormulaError.NUM;
38 | return Math.asin(number);
39 | },
40 |
41 | ASINH: number => {
42 | number = H.accept(number, Types.NUMBER);
43 | return Math.asinh(number);
44 | },
45 |
46 | ATAN: number => {
47 | number = H.accept(number, Types.NUMBER);
48 | return Math.atan(number);
49 | },
50 |
51 | ATAN2: (x, y) => {
52 | x = H.accept(x, Types.NUMBER);
53 | y = H.accept(y, Types.NUMBER);
54 | if (y === 0 && x === 0)
55 | throw FormulaError.DIV0;
56 | return Math.atan2(y, x);
57 | },
58 |
59 | ATANH: number => {
60 | number = H.accept(number, Types.NUMBER);
61 | if (Math.abs(number) > 1)
62 | throw FormulaError.NUM;
63 | return Math.atanh(number);
64 | },
65 |
66 | COS: number => {
67 | number = H.accept(number, Types.NUMBER);
68 | if (Math.abs(number) > MAX_NUMBER)
69 | throw FormulaError.NUM;
70 | return Math.cos(number);
71 | },
72 |
73 | COSH: number => {
74 | number = H.accept(number, Types.NUMBER);
75 | return Math.cosh(number);
76 | },
77 |
78 | COT: number => {
79 | number = H.accept(number, Types.NUMBER);
80 | if (Math.abs(number) > MAX_NUMBER)
81 | throw FormulaError.NUM;
82 | if (number === 0)
83 | throw FormulaError.DIV0;
84 | return 1 / Math.tan(number);
85 | },
86 |
87 | COTH: number => {
88 | number = H.accept(number, Types.NUMBER);
89 | if (number === 0)
90 | throw FormulaError.DIV0;
91 | return 1 / Math.tanh(number);
92 | },
93 |
94 | CSC: number => {
95 | number = H.accept(number, Types.NUMBER);
96 | if (Math.abs(number) > MAX_NUMBER)
97 | throw FormulaError.NUM;
98 | return 1 / Math.sin(number);
99 | },
100 |
101 | CSCH: number => {
102 | number = H.accept(number, Types.NUMBER);
103 | if (number === 0)
104 | throw FormulaError.DIV0;
105 | return 1 / Math.sinh(number);
106 | },
107 |
108 | SEC: number => {
109 | number = H.accept(number, Types.NUMBER);
110 | if (Math.abs(number) > MAX_NUMBER)
111 | throw FormulaError.NUM;
112 | return 1 / Math.cos(number);
113 | },
114 |
115 | SECH: number => {
116 | number = H.accept(number, Types.NUMBER);
117 | return 1 / Math.cosh(number);
118 | },
119 |
120 | SIN: number => {
121 | number = H.accept(number, Types.NUMBER);
122 | if (Math.abs(number) > MAX_NUMBER)
123 | throw FormulaError.NUM;
124 | return Math.sin(number);
125 | },
126 |
127 | SINH: number => {
128 | number = H.accept(number, Types.NUMBER);
129 | return Math.sinh(number);
130 | },
131 |
132 | TAN: number => {
133 | number = H.accept(number, Types.NUMBER);
134 | if (Math.abs(number) > MAX_NUMBER)
135 | throw FormulaError.NUM;
136 | return Math.tan(number);
137 | },
138 |
139 | TANH: number => {
140 | number = H.accept(number, Types.NUMBER);
141 | return Math.tanh(number);
142 | },
143 | };
144 |
145 | module.exports = TrigFunctions;
146 |
--------------------------------------------------------------------------------
/test/grammar/errors.js:
--------------------------------------------------------------------------------
1 | const expect = require('chai').expect;
2 | const FormulaError = require('../../formulas/error');
3 | const {FormulaParser} = require('../../grammar/hooks');
4 | const {DepParser} = require('../../grammar/dependency/hooks');
5 | const {MAX_ROW, MAX_COLUMN} = require('../../index');
6 |
7 | const parser = new FormulaParser({
8 | onCell: ref => {
9 | if (ref.row === 5 && ref.col === 5)
10 | return null
11 | return 1;
12 | },
13 | onRange: ref => {
14 | if (ref.to.row === MAX_ROW) {
15 | return [[1, 2, 3]];
16 | } else if (ref.to.col === MAX_COLUMN) {
17 | return [[1], [0]]
18 | }
19 | return [[1, 2, 3], [0, 0, 0]]
20 | },
21 | functions: {
22 | BAD_FN: () => {
23 | throw new SyntaxError();
24 | }
25 | }
26 | }
27 | );
28 |
29 | const depParser = new DepParser({
30 | onVariable: variable => {
31 | return 'aaaa' === variable ? {from: {row: 1, col: 1}, to: {row: 2, col: 2}} : {row: 1, col: 1};
32 | }
33 | });
34 |
35 | const parsers = [parser, depParser];
36 | const names = ['', ' (DepParser)']
37 |
38 | const position = {row: 1, col: 1, sheet: 'Sheet1'};
39 |
40 | describe('#ERROR! Error handling', () => {
41 | parsers.forEach((parser, idx) => {
42 | it('should handle NotAllInputParsedException' + names[idx], function () {
43 | try {
44 | parser.parse('SUM(1))', position);
45 | } catch (e) {
46 | expect(e).to.be.instanceof(FormulaError);
47 | expect(e.details.errorLocation.line).to.eq(1);
48 | expect(e.details.errorLocation.column).to.eq(7);
49 | expect(e.name).to.eq('#ERROR!');
50 | expect(e.details.name).to.eq('NotAllInputParsedException');
51 | return;
52 | }
53 | throw Error('Should not reach here.');
54 | });
55 |
56 | it('should handle lexing error' + names[idx], function () {
57 | try {
58 | parser.parse('SUM(1)$', position);
59 | } catch (e) {
60 | expect(e).to.be.instanceof(FormulaError);
61 | expect(e.details.errorLocation.line).to.eq(1);
62 | expect(e.details.errorLocation.column).to.eq(7);
63 | expect(e.name).to.eq('#ERROR!');
64 | return;
65 | }
66 | throw Error('Should not reach here.');
67 |
68 | });
69 |
70 | it('should handle Parser error []' + names[idx], function () {
71 | try {
72 | parser.parse('SUM([Sales.xlsx]Jan!B2:B5)', position);
73 | } catch (e) {
74 | expect(e).to.be.instanceof(FormulaError);
75 | expect(e.name).to.eq('#ERROR!');
76 | return;
77 | }
78 | throw Error('Should not reach here.');
79 | });
80 |
81 | it('should handle Parser error' + names[idx], function () {
82 | try {
83 | parser.parse('SUM(B2:B5, "123"+)', position);
84 | } catch (e) {
85 | expect(e).to.be.instanceof(FormulaError);
86 | expect(e.name).to.eq('#ERROR!');
87 | return;
88 | }
89 | throw Error('Should not reach here.');
90 |
91 | });
92 | });
93 |
94 | it('should handle error from functions', function () {
95 | try {
96 | parser.parse('BAD_FN()', position);
97 | } catch (e) {
98 | expect(e).to.be.instanceof(FormulaError);
99 | expect(e.name).to.eq('#ERROR!');
100 | expect(e.details.name).to.eq('SyntaxError');
101 | return;
102 | }
103 | throw Error('Should not reach here.');
104 |
105 | });
106 |
107 | it('should handle errors in async', async function () {
108 | try {
109 | await parser.parseAsync('SUM(*()', position, true);
110 | } catch (e) {
111 | expect(e).to.be.instanceof(FormulaError);
112 | expect(e.name).to.eq('#ERROR!');
113 | return;
114 | }
115 | throw Error('Should not reach here.');
116 | });
117 |
118 | it('should not throw error when ignoreError = true (DepParser)', function () {
119 | try {
120 | depParser.parse('SUM(*()', position, true);
121 | } catch (e) {
122 | throw Error('Should not reach here.');
123 | }
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/logos/jetbrains-variant-4.svg:
--------------------------------------------------------------------------------
1 |
44 |
--------------------------------------------------------------------------------
/test/grammar/test.js:
--------------------------------------------------------------------------------
1 | const expect = require('chai').expect;
2 | const FormulaError = require('../../formulas/error');
3 | const {FormulaParser} = require('../../grammar/hooks');
4 | const {MAX_ROW, MAX_COLUMN} = require('../../index');
5 |
6 | const parser = new FormulaParser({
7 | onCell: ref => {
8 | if (ref.row === 5 && ref.col === 5)
9 | return null
10 | return 1;
11 | },
12 | onRange: ref => {
13 | if (ref.to.row === MAX_ROW) {
14 | return [[1, 2, 3]];
15 | } else if (ref.to.col === MAX_COLUMN) {
16 | return [[1], [0]]
17 | }
18 | return [[1, 2, 3], [0, 0, 0]]
19 | },
20 | }
21 | );
22 | const position = {row: 1, col: 1, sheet: 'Sheet1'};
23 |
24 | describe('Basic parse', () => {
25 | it('should parse null', function () {
26 | let actual = parser.parse('E5', position);
27 | expect(actual).to.deep.eq(null);
28 | });
29 |
30 | it('should parse whole column', function () {
31 | let actual = parser.parse('SUM(A:A)', position);
32 | expect(actual).to.deep.eq(6);
33 | });
34 |
35 | it('should parse whole row', function () {
36 | let actual = parser.parse('SUM(1:1)', position);
37 | expect(actual).to.deep.eq(1);
38 | });
39 |
40 | })
41 |
42 | describe('Parser allows returning array or range', () => {
43 | it('should parse array', function () {
44 | let actual = parser.parse('{1,2,3}', position, true);
45 | expect(actual).to.deep.eq([[1, 2, 3]]);
46 | actual = parser.parse('{1,2,3;4,5,6}', position, true);
47 | expect(actual).to.deep.eq([[1, 2, 3], [4, 5, 6]]);
48 | });
49 |
50 | it('should parse range', function () {
51 | let actual = parser.parse('A1:C1', position, true);
52 | expect(actual).to.deep.eq([[1, 2, 3], [0, 0, 0]]);
53 | });
54 |
55 | it('should not parse unions', function () {
56 | let actual = parser.parse('(A1:C1, A2:E9)', position, true);
57 | expect(actual).to.eq(FormulaError.VALUE);
58 | });
59 |
60 | it('should return single value', function () {
61 | let actual = parser.parse('A1', position, true);
62 | expect(actual).to.eq(1);
63 | });
64 |
65 | it('should return single value', function () {
66 | let actual = parser.parse('E5', position, true);
67 | expect(actual).to.eq(null);
68 | });
69 | });
70 |
71 | describe('async parse', () => {
72 | it('should return single value', async function () {
73 | let actual = await parser.parseAsync('A1', position, true);
74 | expect(actual).to.eq(1);
75 | actual = await parser.parseAsync('E5', position, true);
76 | expect(actual).to.eq(null);
77 | });
78 | });
79 |
80 | describe('Custom async function', () => {
81 | it('should parse and evaluate', async () => {
82 | const parser = new FormulaParser({
83 | onCell: ref => {
84 | return 1;
85 | },
86 | functions: {
87 | IMPORT_CSV: async () => {
88 | return [[1, 2, 3], [4, 5, 6]];
89 | }
90 | },
91 | }
92 | );
93 |
94 | let actual = await parser.parseAsync('A1 + IMPORT_CSV()', position);
95 | expect(actual).to.eq(2);
96 | actual = await parser.parseAsync('-IMPORT_CSV()', position);
97 | expect(actual).to.eq(-1);
98 | actual = await parser.parseAsync('IMPORT_CSV()%', position);
99 | expect(actual).to.eq(0.01);
100 | actual = await parser.parseAsync('SUM(IMPORT_CSV(), 1)', position);
101 | expect(actual).to.eq(22);
102 |
103 | });
104 | it('should support custom function with context', async function () {
105 | const parser = new FormulaParser({
106 | onCell: ref => {
107 | return 1;
108 | },
109 | functionsNeedContext: {
110 | ROW_PLUS_COL: (context) => {
111 | return context.position.row + context.position.col;
112 | }
113 | }
114 | }
115 | );
116 | const actual = await parser.parseAsync('SUM(ROW_PLUS_COL(), 1)', position);
117 | expect(actual).to.eq(3);
118 | });
119 | })
120 |
121 | describe("Github Issues", function () {
122 | it('issue-19: Inconsistent results with parse and parseAsync', async function () {
123 | let res = parser.parse('IF(20 < 0, "yep", "nope")');
124 | expect(res).to.eq('nope');
125 | res = await parser.parseAsync('IF(20 < 0, "yep", "nope")');
126 | expect(res).to.eq('nope');
127 | });
128 | })
129 |
--------------------------------------------------------------------------------
/test/grammar/depParser.js:
--------------------------------------------------------------------------------
1 | const expect = require('chai').expect;
2 | const {DepParser} = require('../../grammar/dependency/hooks');
3 |
4 | const depParser = new DepParser({
5 | onVariable: variable => {
6 | return 'aaaa' === variable ? {from: {row: 1, col: 1}, to: {row: 2, col: 2}} : {row: 1, col: 1};
7 | }
8 | });
9 | const position = {row: 1, col: 1, sheet: 'Sheet1'};
10 |
11 | describe('Dependency parser', () => {
12 | it('parse SUM(1,)', () => {
13 | let actual = depParser.parse('SUM(1,)', position);
14 | expect(actual).to.deep.eq([]);
15 | });
16 |
17 | it('should parse single cell', () => {
18 | let actual = depParser.parse('A1', position);
19 | expect(actual).to.deep.eq([position]);
20 | actual = depParser.parse('A1+1', position);
21 | expect(actual).to.deep.eq([position]);
22 | });
23 |
24 | it('should parse the same cell/range once', () => {
25 | let actual = depParser.parse('A1+A1+A1', position);
26 | expect(actual).to.deep.eq([position]);
27 | actual = depParser.parse('A1:C3+A1:C3+A1:C3', position);
28 | expect(actual).to.deep.eq([{
29 | from: {row: 1, col: 1},
30 | to: {row: 3, col: 3},
31 | sheet: position.sheet
32 | }]);
33 |
34 | actual = depParser.parse('A1:C3+A1:C3+A1:C3+A1+B1', position);
35 | expect(actual).to.deep.eq([{
36 | from: {row: 1, col: 1},
37 | to: {row: 3, col: 3},
38 | sheet: position.sheet
39 | }]);
40 | });
41 |
42 | it('should parse ranges', () => {
43 | let actual = depParser.parse('A1:C3', position);
44 | expect(actual).to.deep.eq([{sheet: 'Sheet1', from: {row: 1, col: 1}, to: {row: 3, col: 3}}]);
45 | actual = depParser.parse('A:C', position);
46 | expect(actual).to.deep.eq([{sheet: 'Sheet1', from: {row: 1, col: 1}, to: {row: 1048576, col: 3}}]);
47 | actual = depParser.parse('1:3', position);
48 | expect(actual).to.deep.eq([{sheet: 'Sheet1', from: {row: 1, col: 1}, to: {row: 3, col: 16384}}]);
49 | });
50 |
51 | it('should parse variable', function () {
52 | let actual = depParser.parse('aaaa', position);
53 | expect(actual).to.deep.eq([{sheet: 'Sheet1', from: {row: 1, col: 1}, to: {row: 2, col: 2}}]);
54 | });
55 |
56 | it('should parse basic formulas', function () {
57 |
58 | // data types
59 | let actual = depParser.parse('TRUE+A1+#VALUE!+{1}', position);
60 | expect(actual).to.deep.eq([{sheet: 'Sheet1', row: 1, col: 1}]);
61 |
62 | // function without args
63 | actual = depParser.parse('A1+FUN()', position);
64 | expect(actual).to.deep.eq([{sheet: 'Sheet1', row: 1, col: 1}]);
65 |
66 | // prefix
67 | actual = depParser.parse('++A1', position);
68 | expect(actual).to.deep.eq([{sheet: 'Sheet1', row: 1, col: 1}]);
69 |
70 | // postfix
71 | actual = depParser.parse('A1%', position);
72 | expect(actual).to.deep.eq([{sheet: 'Sheet1', row: 1, col: 1}]);
73 |
74 | // intersect
75 | actual = depParser.parse('A1:A3 A3:B3', position);
76 | expect(actual).to.deep.eq([{sheet: 'Sheet1', row: 3, col: 1}]);
77 |
78 | // union
79 | actual = depParser.parse('(A1:C1, A2:E9)', position);
80 | expect(actual).to.deep.eq([
81 | {sheet: 'Sheet1', from: {row: 1, col: 1}, to: {row: 1, col: 3}},
82 | {sheet: 'Sheet1', from: {row: 2, col: 1}, to: {row: 9, col: 5}}
83 | ]);
84 | });
85 |
86 | it('should parse complex formula', () => {
87 | let actual = 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);
88 | expect(actual).to.deep.eq([
89 | {
90 | "col": 11,
91 | "row": 1,
92 | "sheet": "Sheet1",
93 | },
94 | {
95 | "col": 1,
96 | "row": 1,
97 | "sheet": "Sheet1",
98 | },
99 | {
100 | "col": 15,
101 | "row": 5,
102 | "sheet": "Sheet1",
103 | },
104 | {
105 | "col": 11,
106 | "row": 3,
107 | "sheet": "Sheet1",
108 | },
109 | ]);
110 | });
111 |
112 | it('should not throw error', function () {
113 | expect((() => depParser.parse('SUM(1))', position, true)))
114 | .to.not.throw();
115 |
116 | expect((() => depParser.parse('SUM(1+)', position, true)))
117 | .to.not.throw();
118 |
119 | expect((() => depParser.parse('SUM(1+)', position, true)))
120 | .to.not.throw();
121 | });
122 | });
123 |
124 |
--------------------------------------------------------------------------------
/formulas/functions/statistical.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers, Types, Criteria, Address} = require('../helpers');
3 | const {Infix} = require('../operators');
4 | const H = FormulaHelpers;
5 | const {DistributionFunctions} = require('./distribution');
6 |
7 | const StatisticalFunctions = {
8 | AVEDEV: (...numbers) => {
9 | let sum = 0;
10 | const arr = [];
11 | // parse number only if the input is literal
12 | H.flattenParams(numbers, Types.NUMBER, true, (item, info) => {
13 | if (typeof item === "number") {
14 | sum += item;
15 | arr.push(item);
16 | }
17 | });
18 | const avg = sum / arr.length;
19 | sum = 0;
20 | for (let i = 0; i < arr.length; i++) {
21 | sum += Math.abs(arr[i] - avg);
22 | }
23 | return sum / arr.length;
24 | },
25 |
26 | AVERAGE: (...numbers) => {
27 | let sum = 0, cnt = 0;
28 | // parse number only if the input is literal
29 | H.flattenParams(numbers, Types.NUMBER, true, (item, info) => {
30 | if (typeof item === "number") {
31 | sum += item;
32 | cnt++;
33 | }
34 | });
35 | return sum / cnt;
36 | },
37 |
38 | AVERAGEA: (...numbers) => {
39 | let sum = 0, cnt = 0;
40 | // parse number only if the input is literal
41 | H.flattenParams(numbers, Types.NUMBER, true, (item, info) => {
42 | const type = typeof item;
43 | if (type === "number") {
44 | sum += item;
45 | cnt++;
46 | } else if (type === "string") {
47 | cnt++;
48 | }
49 | });
50 | return sum / cnt;
51 | },
52 |
53 | // special
54 | AVERAGEIF: (context, range, criteria, averageRange) => {
55 | const ranges = H.retrieveRanges(context, range, averageRange);
56 | range = ranges[0];
57 | averageRange = ranges[1];
58 |
59 | criteria = H.retrieveArg(context, criteria);
60 | const isCriteriaArray = criteria.isArray;
61 | criteria = Criteria.parse(H.accept(criteria));
62 |
63 | let sum = 0, cnt = 0;
64 | range.forEach((row, rowNum) => {
65 | row.forEach((value, colNum) => {
66 | const valueToAdd = averageRange[rowNum][colNum];
67 | if (typeof valueToAdd !== "number")
68 | return;
69 | // wildcard
70 | if (criteria.op === 'wc') {
71 | if (criteria.match === criteria.value.test(value)) {
72 | sum += valueToAdd;
73 | cnt++;
74 | }
75 | } else if (Infix.compareOp(value, criteria.op, criteria.value, Array.isArray(value), isCriteriaArray)) {
76 | sum += valueToAdd;
77 | cnt++;
78 | }
79 | })
80 | });
81 | if (cnt === 0) throw FormulaError.DIV0;
82 | return sum / cnt;
83 | },
84 |
85 | AVERAGEIFS: () => {
86 |
87 | },
88 |
89 | COUNT: (...ranges) => {
90 | let cnt = 0;
91 | H.flattenParams(ranges, null, true,
92 | (item, info) => {
93 | // literal will be parsed to Type.NUMBER
94 | if (info.isLiteral && !isNaN(item)) {
95 | cnt++;
96 | } else {
97 | if (typeof item === "number")
98 | cnt++;
99 | }
100 | });
101 | return cnt;
102 | },
103 |
104 | COUNTIF: (range, criteria) => {
105 | // do not flatten the array
106 | range = H.accept(range, Types.ARRAY, undefined, false, true);
107 | const isCriteriaArray = criteria.isArray;
108 | criteria = H.accept(criteria);
109 |
110 | let cnt = 0;
111 | // parse criteria
112 | criteria = Criteria.parse(criteria);
113 |
114 | range.forEach(row => {
115 | row.forEach(value => {
116 | // wildcard
117 | if (criteria.op === 'wc') {
118 | if (criteria.match === criteria.value.test(value))
119 | cnt++;
120 | } else if (Infix.compareOp(value, criteria.op, criteria.value, Array.isArray(value), isCriteriaArray)) {
121 | cnt++;
122 | }
123 | })
124 | });
125 | return cnt;
126 | },
127 |
128 | LARGE: () => {
129 |
130 | },
131 |
132 | MAX: () => {
133 |
134 | },
135 |
136 | MAXA: () => {
137 |
138 | },
139 |
140 | MAXIFS: () => {
141 |
142 | },
143 |
144 | MEDIAN: () => {
145 |
146 | },
147 |
148 | MIN: () => {
149 |
150 | },
151 |
152 | MINA: () => {
153 |
154 | },
155 |
156 | MINIFS: () => {
157 |
158 | },
159 |
160 | PERMUT: () => {
161 |
162 | },
163 |
164 | PERMUTATIONA: () => {
165 |
166 | },
167 |
168 | SMALL: () => {
169 |
170 | },
171 |
172 | };
173 |
174 |
175 | module.exports = Object.assign(StatisticalFunctions, DistributionFunctions);
176 |
--------------------------------------------------------------------------------
/test/formulas/text/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 |
4 | ASC: {
5 | 'ASC("ABC")': "ABC",
6 | 'ASC("ヲァィゥ")': 'ヲァィゥ',
7 | 'ASC(",。")': ',。',
8 | },
9 |
10 | BAHTTEXT: {
11 | 'BAHTTEXT(63147.89)': 'หกหมื่นสามพันหนึ่งร้อยสี่สิบเจ็ดบาทแปดสิบเก้าสตางค์',
12 | 'BAHTTEXT(1234)': 'หนึ่งพันสองร้อยสามสิบสี่บาทถ้วน',
13 | },
14 |
15 | CHAR: {
16 | 'CHAR(65)': 'A',
17 | 'CHAR(33)': '!',
18 | },
19 |
20 | CLEAN: {
21 | 'CLEAN("äÄçÇéÉêPHP-MySQLöÖÐþúÚ")': "äÄçÇéÉêPHP-MySQLöÖÐþúÚ",
22 | 'CLEAN(CHAR(9)&"Monthly report"&CHAR(10))': 'Monthly report',
23 | },
24 |
25 | CODE: {
26 | 'CODE("C")': 67,
27 | 'CODE("")': FormulaError.VALUE,
28 | },
29 |
30 | CONCAT: {
31 | 'CONCAT(0, {1,2,3;5,6,7})': '0123567',
32 | 'CONCAT(TRUE, 0, {1,2,3;5,6,7})': 'TRUE0123567',
33 | 'CONCAT(0, {1,2,3;5,6,7},)': '0123567',
34 | 'CONCAT("The"," ","sun"," ","will"," ","come"," ","up"," ","tomorrow.")': 'The sun will come up tomorrow.',
35 | 'CONCAT({1,2,3}, "aaa", TRUE, 0, FALSE)': '123aaaTRUE0FALSE'
36 | },
37 |
38 | CONCATENATE: {
39 | 'CONCATENATE({9,8,7})': '9',
40 | 'CONCATENATE({9,8,7},{8,7,6})': '98',
41 | 'CONCATENATE({9,8,7},"hello")': '9hello',
42 | 'CONCATENATE({0,2,3}, 1, "A", TRUE, -12)': '01ATRUE-12',
43 | },
44 |
45 | DBCS: {
46 | 'DBCS("ABC")': "ABC",
47 | 'DBCS("ヲァィゥ")': 'ヲァィゥ',
48 | 'DBCS(",。")': ',。',
49 | },
50 |
51 | DOLLAR: {
52 | 'DOLLAR(1234567)': "$1,234,567.00",
53 | 'DOLLAR(12345.67)': "$12,345.67"
54 | },
55 |
56 | EXACT: {
57 | 'EXACT("hello", "hElLo")': false,
58 | 'EXACT("HELLO","HELLO")': true
59 | },
60 |
61 | FIND: {
62 | 'FIND("h","Hello")': FormulaError.VALUE,
63 | 'FIND("o", "hello")': 5
64 | },
65 |
66 | FIXED: {
67 | 'FIXED(1234.567, 1)': '1,234.6',
68 | 'FIXED(12345.64123213)': '12,345.64',
69 | 'FIXED(12345.64123213, 5)': '12,345.64123',
70 | 'FIXED(12345.64123213, 5, TRUE)': '12345.64123',
71 | 'FIXED(123456789.64, 5, FALSE)': '123,456,789.64000'
72 | },
73 |
74 | LEFT: {
75 | 'LEFT("Salesman")': "S",
76 | 'LEFT("Salesman",4)': "Sale"
77 | },
78 |
79 | RIGHT: {
80 | 'RIGHT("Salseman",3)': "man",
81 | 'RIGHT("Salseman")': "n"
82 | },
83 |
84 | LEN: {
85 | 'LEN("Phoenix, AZ")': 11,
86 | },
87 |
88 | LOWER: {
89 | 'LOWER("E. E. Cummings")': "e. e. cummings"
90 | },
91 |
92 | MID: {
93 | 'MID("Fluid Flow",1,5)': "Fluid",
94 | 'MID("Foo",5,1)': "",
95 | 'MID("Foo",1,5)': "Foo",
96 | 'MID("Foo",-1,5)': FormulaError.VALUE,
97 | 'MID("Foo",1,-5)': FormulaError.VALUE
98 | },
99 |
100 | NUMBERVALUE: {
101 | 'NUMBERVALUE("2.500,27",",",".")': 2500.27,
102 | // group separator occurs before the decimal separator
103 | 'NUMBERVALUE("2500.,27",",",".")': 2500.27,
104 | 'NUMBERVALUE("3.5%")': 0.035,
105 | 'NUMBERVALUE("3 50")': 350,
106 | 'NUMBERVALUE("$3 50")': 350,
107 | 'NUMBERVALUE("($3 50)")': -350,
108 | 'NUMBERVALUE("-($3 50)")': FormulaError.VALUE,
109 | 'NUMBERVALUE("($-3 50)")': FormulaError.VALUE,
110 | 'NUMBERVALUE("2500,.27",",",".")': FormulaError.VALUE,
111 | // group separator occurs after the decimal separator
112 | 'NUMBERVALUE("3.5%",".",".")': FormulaError.VALUE,
113 | 'NUMBERVALUE("3.5%",,)': FormulaError.VALUE,
114 | // decimal separator is used more than once
115 | 'NUMBERVALUE("3..5")': FormulaError.VALUE,
116 |
117 | },
118 |
119 | PROPER: {
120 | 'PROPER("this is a tiTle")': "This Is A Title",
121 | 'PROPER("2-way street")': "2-Way Street",
122 | 'PROPER("76BudGet")': '76Budget',
123 | },
124 |
125 | REPLACE: {
126 | 'REPLACE("abcdefghijk",6,5,"*")': "abcde*k",
127 | 'REPLACE("abcdefghijk",6,0,"*")': "abcde*fghijk"
128 | },
129 |
130 | REPT: {
131 | 'REPT("*_",4)': "*_*_*_*_"
132 | },
133 |
134 | SEARCH: {
135 | 'SEARCH(",", "abcdef")': FormulaError.VALUE,
136 | 'SEARCH("b", "abcdef")': 2,
137 | 'SEARCH("c*f", "abcdef")': 3,
138 | 'SEARCH("c?f", "abcdef")': FormulaError.VALUE,
139 | 'SEARCH("c?e", "abcdef")': 3,
140 | 'SEARCH("c\\b", "abcabcac\\bacb", 6)': 8,
141 | },
142 |
143 | T: {
144 | 'T("*_")': "*_",
145 | 'T(19)': "",
146 | },
147 |
148 | TEXT: {
149 | 'TEXT(1234.567,"$#,##0.00")': "$1,234.57",
150 | },
151 |
152 | TRIM: {
153 | 'TRIM(" First Quarter Earnings ")': "First Quarter Earnings"
154 | },
155 |
156 | UNICHAR: {
157 | 'UNICHAR(32)': " ",
158 | 'UNICHAR(66)': "B",
159 | 'UNICHAR(0)': FormulaError.VALUE,
160 | 'UNICHAR(3333)': 'അ',
161 | },
162 |
163 | UNICODE: {
164 | 'UNICODE(" ")': 32,
165 | 'UNICODE("B")': 66,
166 | 'UNICODE("")': FormulaError.VALUE,
167 | },
168 |
169 | UPPER: {
170 | 'UPPER("E. E. Cummings")': "E. E. CUMMINGS"
171 | },
172 |
173 | };
174 |
--------------------------------------------------------------------------------
/grammar/dependency/hooks.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../formulas/error');
2 | const {FormulaHelpers} = require('../../formulas/helpers');
3 | const {Parser} = require('../parsing');
4 | const lexer = require('../lexing');
5 | const Utils = require('./utils');
6 | const {formatChevrotainError} = require('../utils');
7 |
8 | class DepParser {
9 |
10 | /**
11 | *
12 | * @param {{onVariable: Function}} [config]
13 | */
14 | constructor(config) {
15 | this.data = [];
16 | this.utils = new Utils(this);
17 | config = Object.assign({
18 | onVariable: () => null,
19 | }, config);
20 | this.utils = new Utils(this);
21 |
22 | this.onVariable = config.onVariable;
23 | this.functions = {}
24 |
25 | this.parser = new Parser(this, this.utils);
26 | }
27 |
28 | /**
29 | * Get value from the cell reference
30 | * @param ref
31 | * @return {*}
32 | */
33 | getCell(ref) {
34 | // console.log('get cell', JSON.stringify(ref));
35 | if (ref.row != null) {
36 | if (ref.sheet == null)
37 | ref.sheet = this.position ? this.position.sheet : undefined;
38 | const idx = this.data.findIndex(element => {
39 | return (element.from && element.from.row <= ref.row && element.to.row >= ref.row
40 | && element.from.col <= ref.col && element.to.col >= ref.col)
41 | || (element.row === ref.row && element.col === ref.col && element.sheet === ref.sheet)
42 | });
43 | if (idx === -1)
44 | this.data.push(ref);
45 | }
46 | return 0;
47 | }
48 |
49 | /**
50 | * Get values from the range reference.
51 | * @param ref
52 | * @return {*}
53 | */
54 | getRange(ref) {
55 | // console.log('get range', JSON.stringify(ref));
56 | if (ref.from.row != null) {
57 | if (ref.sheet == null)
58 | ref.sheet = this.position ? this.position.sheet : undefined;
59 |
60 | const idx = this.data.findIndex(element => {
61 | return element.from && element.from.row === ref.from.row && element.from.col === ref.from.col
62 | && element.to.row === ref.to.row && element.to.col === ref.to.col;
63 | });
64 | if (idx === -1)
65 | this.data.push(ref);
66 | }
67 | return [[0]]
68 | }
69 |
70 | /**
71 | * TODO:
72 | * Get references or values from a user defined variable.
73 | * @param name
74 | * @return {*}
75 | */
76 | getVariable(name) {
77 | // console.log('get variable', name);
78 | const res = {ref: this.onVariable(name, this.position.sheet)};
79 | if (res.ref == null)
80 | return FormulaError.NAME;
81 | if (FormulaHelpers.isCellRef(res))
82 | this.getCell(res.ref);
83 | else {
84 | this.getRange(res.ref);
85 | }
86 | return 0;
87 | }
88 |
89 | /**
90 | * Retrieve values from the given reference.
91 | * @param valueOrRef
92 | * @return {*}
93 | */
94 | retrieveRef(valueOrRef) {
95 | if (FormulaHelpers.isRangeRef(valueOrRef)) {
96 | return this.getRange(valueOrRef.ref);
97 | }
98 | if (FormulaHelpers.isCellRef(valueOrRef)) {
99 | return this.getCell(valueOrRef.ref)
100 | }
101 | return valueOrRef;
102 | }
103 |
104 | /**
105 | * Call an excel function.
106 | * @param name - Function name.
107 | * @param args - Arguments that pass to the function.
108 | * @return {*}
109 | */
110 | callFunction(name, args) {
111 | args.forEach(arg => {
112 | if (arg == null)
113 | return;
114 | this.retrieveRef(arg);
115 | });
116 | return {value: 0, ref: {}};
117 | }
118 |
119 | /**
120 | * Check and return the appropriate formula result.
121 | * @param result
122 | * @return {*}
123 | */
124 | checkFormulaResult(result) {
125 | this.retrieveRef(result);
126 | }
127 |
128 | /**
129 | * Parse an excel formula and return the dependencies
130 | * @param {string} inputText
131 | * @param {{row: number, col: number, sheet: string}} position
132 | * @param {boolean} [ignoreError=false] if true, throw FormulaError when error occurred.
133 | * if false, the parser will return partial dependencies.
134 | * @returns {Array.<{}>}
135 | */
136 | parse(inputText, position, ignoreError = false) {
137 | if (inputText.length === 0) throw Error('Input must not be empty.');
138 | this.data = [];
139 | this.position = position;
140 | const lexResult = lexer.lex(inputText);
141 | this.parser.input = lexResult.tokens;
142 | try {
143 | const res = this.parser.formulaWithBinaryOp();
144 | this.checkFormulaResult(res);
145 | } catch (e) {
146 | if (!ignoreError) {
147 | throw FormulaError.ERROR(e.message, e);
148 | }
149 | }
150 | if (this.parser.errors.length > 0 && !ignoreError) {
151 | const error = this.parser.errors[0];
152 | throw formatChevrotainError(error, inputText);
153 | }
154 |
155 | return this.data;
156 | }
157 | }
158 |
159 | module.exports = {
160 | DepParser,
161 | };
162 |
--------------------------------------------------------------------------------
/formulas/operators.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../formulas/error');
2 | const {FormulaHelpers} = require('../formulas/helpers');
3 |
4 | const Prefix = {
5 | unaryOp: (prefixes, value, isArray) => {
6 | let sign = 1;
7 | prefixes.forEach(prefix => {
8 | if (prefix === '+') {
9 | } else if (prefix === '-') {
10 | sign = -sign;
11 | } else {
12 | throw new Error(`Unrecognized prefix: ${prefix}`);
13 | }
14 | });
15 |
16 | if (value == null) {
17 | value = 0;
18 | }
19 | // positive means no changes
20 | if (sign === 1) {
21 | return value;
22 | }
23 | // negative
24 | try {
25 | value = FormulaHelpers.acceptNumber(value, isArray);
26 | } catch (e) {
27 | if (e instanceof FormulaError) {
28 | // parse number fails
29 | if (Array.isArray(value))
30 | value = value[0][0]
31 | } else
32 | throw e;
33 | }
34 |
35 | if (typeof value === "number" && isNaN(value)) return FormulaError.VALUE;
36 | return -value;
37 | }
38 | };
39 |
40 | const Postfix = {
41 | percentOp: (value, postfix, isArray) => {
42 | try {
43 | value = FormulaHelpers.acceptNumber(value, isArray);
44 | } catch (e) {
45 | if (e instanceof FormulaError)
46 | return e;
47 | throw e;
48 | }
49 | if (postfix === '%') {
50 | return value / 100;
51 | }
52 | throw new Error(`Unrecognized postfix: ${postfix}`);
53 | }
54 | };
55 |
56 | const type2Number = {'boolean': 3, 'string': 2, 'number': 1};
57 |
58 | const Infix = {
59 | compareOp: (value1, infix, value2, isArray1, isArray2) => {
60 | if (value1 == null) value1 = 0;
61 | if (value2 == null) value2 = 0;
62 | // for array: {1,2,3}, get the first element to compare
63 | if (isArray1) {
64 | value1 = value1[0][0];
65 | }
66 | if (isArray2) {
67 | value2 = value2[0][0];
68 | }
69 |
70 | const type1 = typeof value1, type2 = typeof value2;
71 |
72 | if (type1 === type2) {
73 | // same type comparison
74 | switch (infix) {
75 | case '=':
76 | return value1 === value2;
77 | case '>':
78 | return value1 > value2;
79 | case '<':
80 | return value1 < value2;
81 | case '<>':
82 | return value1 !== value2;
83 | case '<=':
84 | return value1 <= value2;
85 | case '>=':
86 | return value1 >= value2;
87 | }
88 | } else {
89 | switch (infix) {
90 | case '=':
91 | return false;
92 | case '>':
93 | return type2Number[type1] > type2Number[type2];
94 | case '<':
95 | return type2Number[type1] < type2Number[type2];
96 | case '<>':
97 | return true;
98 | case '<=':
99 | return type2Number[type1] <= type2Number[type2];
100 | case '>=':
101 | return type2Number[type1] >= type2Number[type2];
102 | }
103 |
104 | }
105 | throw Error('Infix.compareOp: Should not reach here.');
106 | },
107 |
108 | concatOp: (value1, infix, value2, isArray1, isArray2) => {
109 | if (value1 == null) value1 = '';
110 | if (value2 == null) value2 = '';
111 | // for array: {1,2,3}, get the first element to concat
112 | if (isArray1) {
113 | value1 = value1[0][0];
114 | }
115 | if (isArray2) {
116 | value2 = value2[0][0];
117 | }
118 |
119 | const type1 = typeof value1, type2 = typeof value2;
120 | // convert boolean to string
121 | if (type1 === 'boolean')
122 | value1 = value1 ? 'TRUE' : 'FALSE';
123 | if (type2 === 'boolean')
124 | value2 = value2 ? 'TRUE' : 'FALSE';
125 | return '' + value1 + value2;
126 | },
127 |
128 | mathOp: (value1, infix, value2, isArray1, isArray2) => {
129 | if (value1 == null) value1 = 0;
130 | if (value2 == null) value2 = 0;
131 |
132 | try {
133 | value1 = FormulaHelpers.acceptNumber(value1, isArray1);
134 | value2 = FormulaHelpers.acceptNumber(value2, isArray2);
135 | } catch (e) {
136 | if (e instanceof FormulaError)
137 | return e;
138 | throw e;
139 | }
140 |
141 | switch (infix) {
142 | case '+':
143 | return value1 + value2;
144 | case '-':
145 | return value1 - value2;
146 | case '*':
147 | return value1 * value2;
148 | case '/':
149 | if (value2 === 0)
150 | return FormulaError.DIV0;
151 | return value1 / value2;
152 | case '^':
153 | return value1 ** value2;
154 | }
155 |
156 | throw Error('Infix.mathOp: Should not reach here.');
157 | },
158 |
159 | };
160 |
161 | module.exports = {
162 | Prefix,
163 | Postfix,
164 | Infix,
165 | Operators: {
166 | compareOp: ['<', '>', '=', '<>', '<=', '>='],
167 | concatOp: ['&'],
168 | mathOp: ['+', '-', '*', '/', '^'],
169 | }
170 | };
171 |
--------------------------------------------------------------------------------
/test/formulas/reference/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 | ADDRESS: {
4 | 'ADDRESS(2,3)': '$C$2',
5 | 'ADDRESS(2,3, 1)': '$C$2',
6 | 'ADDRESS(2,3,2)': 'C$2',
7 | 'ADDRESS(2,3,3)': '$C2',
8 | 'ADDRESS(2,3,4, TRUE)': 'C2',
9 | 'ADDRESS(2,3,2,FALSE)': 'R2C[3]',
10 | 'ADDRESS(2,3,1,FALSE,"[Book1]Sheet1")': "'[Book1]Sheet1'!R2C3",
11 | 'ADDRESS(2,3,1,FALSE,"EXCEL SHEET")': "'EXCEL SHEET'!R2C3",
12 | 'ADDRESS(2,3,4, 2, "abc")': 'abc!C2',
13 | },
14 |
15 | AREAS: {
16 | 'AREAS(B2:D4)': 1,
17 | 'AREAS((B2:D4,E5,F6:I9))': 3,
18 | 'AREAS(B2:D4 B2)': 1,
19 | },
20 |
21 | COLUMN: {
22 | 'COLUMN()': 1,
23 | 'COLUMN(C3)': 3,
24 | 'COLUMN(C3:V6)': 3,
25 | 'COLUMN(123)': FormulaError.VALUE,
26 | 'COLUMN({1,2,3})': FormulaError.VALUE,
27 | 'COLUMN("A1")': FormulaError.VALUE
28 | },
29 |
30 | COLUMNS: {
31 | 'COLUMNS(A1)': 1,
32 | 'COLUMNS(A1:C5)': 3,
33 | 'COLUMNS(123)': FormulaError.VALUE,
34 | 'COLUMNS({1,2,3})': FormulaError.VALUE,
35 | 'COLUMNS("A1")': FormulaError.VALUE
36 | },
37 |
38 | HLOOKUP: {
39 | 'HLOOKUP(3, {1,2,3,4,5}, 1)': 3,
40 | 'HLOOKUP(3, {3,2,1}, 1)': 1,
41 | 'HLOOKUP(3, {1,2,3,4,5}, 2)': FormulaError.REF,
42 | 'HLOOKUP("a", {1,2,3,4,5}, 1)': FormulaError.NA,
43 | 'HLOOKUP(3, {1.1,2.2,3.3,4.4,5.5}, 1)': 2.2,
44 | // should handle like Excel.
45 | 'HLOOKUP(63, {"c",FALSE,"abc",65,63,61,"b","a",FALSE,TRUE}, 1)': 63,
46 | 'HLOOKUP(TRUE, {"c",FALSE,"abc",65,63,61,"b","a",FALSE,TRUE}, 1)': true,
47 | 'HLOOKUP(FALSE, {"c",FALSE,"abc",65,63,61,"b","a",FALSE,TRUE}, 1)': false,
48 | 'HLOOKUP(FALSE, {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': FormulaError.NA,
49 | 'HLOOKUP("c", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': 'a',
50 | 'HLOOKUP("b", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': 'b',
51 | 'HLOOKUP("abc", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': 'abc',
52 | 'HLOOKUP("a", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': FormulaError.NA,
53 | 'HLOOKUP("a*", {"c",TRUE,"abc",65,63,61,"b","a",TRUE,FALSE}, 1)': FormulaError.NA,
54 | // with rangeLookup = FALSE
55 | 'HLOOKUP(3, 3, 1,FALSE)': FormulaError.NA,
56 | 'HLOOKUP(3, {1,2,3}, 1,FALSE)': 3,
57 | 'HLOOKUP("a", {1,2,3,"a","b"}, 1,FALSE)': 'a',
58 | 'HLOOKUP(3, {1,2,3;"a","b","c"}, 2,FALSE)': 'c',
59 | 'HLOOKUP(6, {1,2,3;"a","b","c"}, 2,FALSE)': FormulaError.NA,
60 | // wildcard support
61 | 'HLOOKUP("s?", {"abc", "sd", "qwe"}, 1,FALSE)': 'sd',
62 | 'HLOOKUP("*e", {"abc", "sd", "qwe"}, 1,FALSE)': 'qwe',
63 | 'HLOOKUP("*e?2?", {"abc", "sd", "qwe123"}, 1,FALSE)': 'qwe123',
64 | // case insensitive
65 | 'HLOOKUP("a*", {"c",TRUE,"AbC",65,63,61,"b","a",TRUE,FALSE}, 1, FALSE)': 'AbC',
66 | // single row table
67 | 'HLOOKUP(614, { 614;"Foobar"}, 2)': 'Foobar'
68 | },
69 |
70 | INDEX: {
71 | 'INDEX(A11:B12,2,2)': 'Pears',
72 | 'INDEX(A11:B12,2,1)': 'Bananas',
73 | 'INDEX({1,2;3,4},1,2)': 2,
74 | 'INDEX(A2:C6, 2, 3)': 38,
75 | 'INDEX((A1:C6, A8:C11), 2, 2, 2)': 1.25,
76 |
77 | 'SUM(INDEX(A1:C11, 0, 3, 1))': 216,
78 | 'SUM(INDEX(A1:E11, 1, 0, 1))': 9,
79 | 'SUM(INDEX(A1:E11, 1, 0, 2))': FormulaError.REF,
80 | 'SUM(B2:INDEX(A2:C6, 5, 2))': 2.42,
81 | 'SUM(B2:IF(TRUE, INDEX(A2:C6, 5, 2)))': 2.42,
82 | 'SUM(INDEX(D1:E2, 0, 0, 1))': 20,
83 | },
84 |
85 | ROW: {
86 | 'ROW()': 1,
87 | 'ROW(C4)': 4,
88 | 'ROW(C4:V6)': 4,
89 | 'ROW(123)': FormulaError.VALUE,
90 | 'ROW({1,2,3})': FormulaError.VALUE,
91 | 'ROW("A1")': FormulaError.VALUE
92 | },
93 |
94 | ROWS: {
95 | 'ROWS(A1)': 1,
96 | 'ROWS(A1:C5)': 5,
97 | 'ROWS(123)': FormulaError.VALUE,
98 | 'ROWS({1,2,3})': FormulaError.VALUE,
99 | 'ROWS("A1")': FormulaError.VALUE
100 | },
101 |
102 | TRANSPOSE: {
103 | // this should be good, lol
104 | 'SUM(TRANSPOSE({1,2,3;4,5,6}))': 21,
105 | },
106 |
107 | VLOOKUP: {
108 | 'VLOOKUP(3, {1;2;3;4;5}, 1)': 3,
109 | 'VLOOKUP(3, {3;2;1}, 1)': 1,
110 | 'VLOOKUP(3, {1;2;3;4;5}, 2)': FormulaError.REF,
111 | 'VLOOKUP("a", {1;2;3;4;5}, 1)': FormulaError.NA,
112 | 'VLOOKUP(3, {1.1;2.2;3.3;4.4;5.5}, 1)': 2.2,
113 | // should handle like Excel.
114 | 'VLOOKUP(63, {"c";FALSE;"abc";65;63;61;"b";"a";FALSE;TRUE}, 1)': 63,
115 | 'VLOOKUP(TRUE, {"c";FALSE;"abc";65;63;61;"b";"a";FALSE;TRUE}, 1)': true,
116 | 'VLOOKUP(FALSE, {"c";FALSE;"abc";65;63;61;"b";"a";FALSE;TRUE}, 1)': false,
117 | 'VLOOKUP(FALSE, {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': FormulaError.NA,
118 | 'VLOOKUP("c", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': 'a',
119 | 'VLOOKUP("b", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': 'b',
120 | 'VLOOKUP("abc", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': 'abc',
121 | 'VLOOKUP("a", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': FormulaError.NA,
122 | 'VLOOKUP("a*", {"c";TRUE;"abc";65;63;61;"b";"a";TRUE;FALSE}, 1)': FormulaError.NA,
123 | // with rangeLookup = FALSE
124 | 'VLOOKUP(3, 3, 1,FALSE)': FormulaError.NA,
125 | 'VLOOKUP(3, {1;2;3}, 1,FALSE)': 3,
126 | 'VLOOKUP("a", {1;2;3;"a";"b"}, 1,FALSE)': 'a',
127 | 'VLOOKUP(3, {1,"a";2, "b";3, "c"}, 2,FALSE)': 'c',
128 | 'VLOOKUP(6, {1,"a";2, "b";3, "c"}, 2,FALSE)': FormulaError.NA,
129 | // wildcard support
130 | 'VLOOKUP("s?", {"abc"; "sd"; "qwe"}, 1,FALSE)': 'sd',
131 | 'VLOOKUP("*e", {"abc"; "sd"; "qwe"}, 1,FALSE)': 'qwe',
132 | 'VLOOKUP("*e?2?", {"abc"; "sd"; "qwe123"}, 1,FALSE)': 'qwe123',
133 | // case insensitive
134 | 'VLOOKUP("a*", {"c";TRUE;"AbC";65;63;61;"b";"a";TRUE;FALSE}, 1, FALSE)': 'AbC',
135 | // single row table
136 | 'VLOOKUP(614, { 614,"Foobar"}, 2)': 'Foobar'
137 | }
138 |
139 | };
140 |
--------------------------------------------------------------------------------
/grammar/lexing.js:
--------------------------------------------------------------------------------
1 | const {createToken, Lexer} = require('chevrotain');
2 | const FormulaError = require('../formulas/error')
3 |
4 | // the vocabulary will be exported and used in the Parser definition.
5 | const tokenVocabulary = {};
6 |
7 | const WhiteSpace = createToken({
8 | name: 'WhiteSpace',
9 | pattern: /\s+/,
10 | group: Lexer.SKIPPED,
11 | });
12 |
13 | const String = createToken({
14 | name: 'String',
15 | pattern: /"(""|[^"])*"/
16 | });
17 |
18 | const SingleQuotedString = createToken({
19 | name: 'SingleQuotedString',
20 | pattern: /'(''|[^'])*'/
21 | });
22 |
23 | const SheetQuoted = createToken({
24 | name: 'SheetQuoted',
25 | pattern: /'((?![\\\/\[\]*?:]).)+?'!/
26 | });
27 |
28 | const Function = createToken({
29 | name: 'Function',
30 | pattern: /[A-Za-z_]+[A-Za-z_0-9.]*\(/
31 | });
32 |
33 | const FormulaErrorT = createToken({
34 | name: 'FormulaErrorT',
35 | pattern: /#NULL!|#DIV\/0!|#VALUE!|#NAME\?|#NUM!|#N\/A/
36 | });
37 |
38 | const RefError = createToken({
39 | name: 'RefError',
40 | pattern: /#REF!/
41 | });
42 |
43 | const Name = createToken({
44 | name: 'Name',
45 | pattern: /[a-zA-Z_][a-zA-Z0-9_.?]*/,
46 | // longer_alt: RangeColumn // e.g. A:AA
47 | });
48 |
49 | const Sheet = createToken({
50 | name: 'Sheet',
51 | pattern: /[A-Za-z_.\d\u007F-\uFFFF]+!/
52 | });
53 |
54 | const Cell = createToken({
55 | name: 'Cell',
56 | pattern: /[$]?[A-Za-z]{1,3}[$]?[1-9][0-9]*/,
57 | longer_alt: Name
58 | });
59 |
60 | const Number = createToken({
61 | name: 'Number',
62 | pattern: /[0-9]+[.]?[0-9]*([eE][+\-][0-9]+)?/
63 | });
64 |
65 | const Boolean = createToken({
66 | name: 'Boolean',
67 | pattern: /TRUE|FALSE/i
68 | });
69 |
70 | const Column = createToken({
71 | name: 'Column',
72 | pattern: /[$]?[A-Za-z]{1,3}/,
73 | longer_alt: Name
74 | });
75 |
76 |
77 | /**
78 | * Symbols and operators
79 | */
80 | const At = createToken({
81 | name: 'At',
82 | pattern: /@/
83 | });
84 |
85 | const Comma = createToken({
86 | name: 'Comma',
87 | pattern: /,/
88 | });
89 |
90 | const Colon = createToken({
91 | name: 'Colon',
92 | pattern: /:/
93 | });
94 |
95 | const Semicolon = createToken({
96 | name: 'Semicolon',
97 | pattern: /;/
98 | });
99 |
100 | const OpenParen = createToken({
101 | name: 'OpenParen',
102 | pattern: /\(/
103 | });
104 |
105 | const CloseParen = createToken({
106 | name: 'CloseParen',
107 | pattern: /\)/
108 | });
109 |
110 | const OpenSquareParen = createToken({
111 | name: 'OpenSquareParen',
112 | pattern: /\[/
113 | });
114 |
115 | const CloseSquareParen = createToken({
116 | name: 'CloseSquareParen',
117 | pattern: /]/
118 | });
119 |
120 | const ExclamationMark = createToken({
121 | name: 'exclamationMark',
122 | pattern: /!/
123 | });
124 |
125 | const OpenCurlyParen = createToken({
126 | name: 'OpenCurlyParen',
127 | pattern: /{/
128 | });
129 |
130 | const CloseCurlyParen = createToken({
131 | name: 'CloseCurlyParen',
132 | pattern: /}/
133 | });
134 |
135 | const QuoteS = createToken({
136 | name: 'QuoteS',
137 | pattern: /'/
138 | });
139 |
140 |
141 | const MulOp = createToken({
142 | name: 'MulOp',
143 | pattern: /\*/
144 | });
145 |
146 | const PlusOp = createToken({
147 | name: 'PlusOp',
148 | pattern: /\+/
149 | });
150 |
151 | const DivOp = createToken({
152 | name: 'DivOp',
153 | pattern: /\//
154 | });
155 |
156 | const MinOp = createToken({
157 | name: 'MinOp',
158 | pattern: /-/
159 | });
160 |
161 | const ConcatOp = createToken({
162 | name: 'ConcatOp',
163 | pattern: /&/
164 | });
165 |
166 | const ExOp = createToken({
167 | name: 'ExOp',
168 | pattern: /\^/
169 | });
170 |
171 | const PercentOp = createToken({
172 | name: 'PercentOp',
173 | pattern: /%/
174 | });
175 |
176 | const GtOp = createToken({
177 | name: 'GtOp',
178 | pattern: />/
179 | });
180 |
181 | const EqOp = createToken({
182 | name: 'EqOp',
183 | pattern: /=/
184 | });
185 |
186 | const LtOp = createToken({
187 | name: 'LtOp',
188 | pattern: /
189 | });
190 |
191 | const NeqOp = createToken({
192 | name: 'NeqOp',
193 | pattern: /<>/
194 | });
195 |
196 | const GteOp = createToken({
197 | name: 'GteOp',
198 | pattern: />=/
199 | });
200 |
201 | const LteOp = createToken({
202 | name: 'LteOp',
203 | pattern: /<=/
204 | });
205 |
206 | // The order of tokens is important
207 | const allTokens = [
208 |
209 | WhiteSpace,
210 | String,
211 | SheetQuoted,
212 | SingleQuotedString,
213 | Function,
214 | FormulaErrorT,
215 | RefError,
216 | Sheet,
217 | Cell,
218 | Boolean,
219 | Column,
220 | Name,
221 | Number,
222 |
223 | At,
224 | Comma,
225 | Colon,
226 | Semicolon,
227 | OpenParen,
228 | CloseParen,
229 | OpenSquareParen,
230 | CloseSquareParen,
231 | // ExclamationMark,
232 | OpenCurlyParen,
233 | CloseCurlyParen,
234 | QuoteS,
235 | MulOp,
236 | PlusOp,
237 | DivOp,
238 | MinOp,
239 | ConcatOp,
240 | ExOp,
241 | MulOp,
242 | PercentOp,
243 | NeqOp,
244 | GteOp,
245 | LteOp,
246 | GtOp,
247 | EqOp,
248 | LtOp,
249 | ];
250 |
251 | const SelectLexer = new Lexer(allTokens, {ensureOptimizations: true});
252 |
253 | allTokens.forEach(tokenType => {
254 | tokenVocabulary[tokenType.name] = tokenType
255 | });
256 |
257 | module.exports = {
258 | tokenVocabulary: tokenVocabulary,
259 |
260 | lex: function (inputText) {
261 | const lexingResult = SelectLexer.tokenize(inputText)
262 |
263 | if (lexingResult.errors.length > 0) {
264 | const error = lexingResult.errors[0];
265 | const line = error.line, column = error.column;
266 | let msg = '\n' + inputText.split('\n')[line - 1] + '\n';
267 | msg += Array(column - 1).fill(' ').join('') + '^\n';
268 | error.message = msg + `Error at position ${line}:${column}\n` + error.message;
269 | error.errorLocation = {line, column};
270 | throw FormulaError.ERROR(error.message, error);
271 | }
272 |
273 | return lexingResult
274 | }
275 | };
276 |
--------------------------------------------------------------------------------
/test/formulas/date/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 | DATE: {
4 | 'DATE(108,1,2)': 39449,
5 | 'DATE(1,1,2)': 368,
6 | 'DATE(2008,1,2)': 39449,
7 | 'DATE(2008,14,2)': 39846,
8 | 'DATE(2008,-3,2)': 39327,
9 | 'DATE(2008,1,35)': 39482,
10 | 'DATE(2008,1,-15)': 39432,
11 | 'DATE(-1,1,2)': FormulaError.NUM,
12 | 'DATE(10000,1,2)': FormulaError.NUM,
13 | },
14 |
15 | DATEDIF: {
16 | 'DATEDIF("1/1/2001","1/1/2003","Y")': 2,
17 | 'DATEDIF("1/2/2001","1/1/2003","Y")': 1,
18 |
19 | 'DATEDIF("6/1/2001","8/15/2002","M")': 14,
20 | 'DATEDIF("6/16/2001","8/15/2002","M")': 13,
21 | 'DATEDIF("9/15/2001","8/15/2003","M")': 23,
22 |
23 | 'DATEDIF("6/1/2001","8/15/2002","D")': 440,
24 | 'DATEDIF("6/1/2001","6/1/2002","D")': 365,
25 |
26 | 'DATEDIF("6/1/2001","8/15/2002","MD")': 14,
27 | 'DATEDIF("8/16/2001","8/15/2002","MD")': 30,
28 | 'DATEDIF("5/16/2001","7/15/2002","MD")': 29,
29 | 'DATEDIF("5/15/2001","7/15/2002","MD")': 0,
30 |
31 | 'DATEDIF("6/1/2001","8/15/2003","YM")': 2,
32 | 'DATEDIF("6/16/2001","8/15/2003","YM")': 1,
33 | 'DATEDIF("9/15/2001","8/15/2003","YM")': 11,
34 | 'DATEDIF("9/15/2001","9/15/2003","YM")': 0,
35 |
36 | 'DATEDIF("6/1/2001","8/15/2002","YD")': 75,
37 | 'DATEDIF("9/15/2001","8/15/2003","YD")': 334,
38 | 'DATEDIF("8/15/2001","8/15/2003","YD")': 0,
39 | 'DATEDIF("8/14/2001","8/15/2003","YD")': 1,
40 |
41 | 'DATEDIF("8/14/2005","8/15/2003","YD")': FormulaError.NUM,
42 |
43 | },
44 |
45 | DATEVALUE: {
46 | 'DATEVALUE("1/1/2008")': 39448,
47 | 'DATEVALUE("1-Jan-2008")': 39448,
48 | 'DATEVALUE("January 1, 2008")': 39448,
49 | 'DATEVALUE("8/22/2011")': 40777,
50 | 'DATEVALUE("22-MAY-2011")': 40685,
51 | 'DATEVALUE("2011/02/23")': 40597,
52 | 'DATEVALUE("December 31, 9999")': 2958465,
53 | 'DATEVALUE("11" & "/" & "3" & "/" & "2011")': 40850,
54 |
55 | // all formats
56 | // TODO: Update results every year :(
57 | 'DATEVALUE("12/3/2014")': 41976,
58 | 'DATEVALUE("Wednesday, December 3, 2014")': 41976,
59 | 'DATEVALUE("2014-12-3")': 41976,
60 | 'DATEVALUE("12/3/14")': 41976,
61 | 'DATEVALUE("12/03/14")': 41976,
62 | 'DATEVALUE("3-Dec-14")': 41976,
63 | 'DATEVALUE("03-Dec-14")': 41976,
64 | 'DATEVALUE("12/3")': 45629, // *special
65 | 'DATEVALUE("3-Dec")': 45629, // *special
66 | 'DATEVALUE("Dec-3")': 45629, // *special
67 | 'DATEVALUE("December-3")': 45629, // *special
68 | 'DATEVALUE("Dec-3 11:11")': 45629, // *special
69 | 'DATEVALUE("December 3, 2014")': 41976,
70 | 'DATEVALUE("12/3/14 12:00 AM")': 41976,
71 | 'DATEVALUE("12/3/14 0:00")': 41976,
72 | 'DATEVALUE("12/03/2014")': 41976,
73 | 'DATEVALUE("3-Dec-2014")': 41976,
74 | 'DATEVALUE("1900/1/1")': 1,
75 | 'DATEVALUE("4:48:18 PM")': 0,
76 | 'DATEVALUE("10000/12/1")': FormulaError.VALUE,
77 | },
78 |
79 | DAY: {
80 | 'DAY(DATEVALUE("15-Apr-11"))': 15,
81 | 'DAY("15-Apr-11")': 15,
82 | 'DAY(-12)': FormulaError.VALUE,
83 | },
84 |
85 | DAYS: {
86 | 'DAYS("2020/3/1", "2020/2/1")': 29,
87 | 'DAYS("3/15/11","2/1/11")': 42,
88 | 'DAYS(DATEVALUE("3/15/11"),DATEVALUE("2/1/11"))': 42,
89 | 'DAYS("12/31/2011","1/1/2011")': 364,
90 | 'DAYS(DATEVALUE("12/31/2011"),DATEVALUE("1/1/2011"))': 364,
91 | },
92 |
93 | DAYS360: {
94 | 'DAYS360("2/1/2019", "2/28/2019")': 27,
95 | 'DAYS360("2/1/2019", "3/1/2019")': 30,
96 | 'DAYS360("1/31/2019", "3/31/2019")': 60,
97 | 'DAYS360("2/1/2019", "3/31/2019")': 60,
98 | 'DAYS360("2/1/2019", "3/31/2019", TRUE)': 59,
99 | 'DAYS360("3/31/2019", "3/31/2019")': 0,
100 | 'DAYS360("3/31/2019", "3/31/2019", TRUE)': 0,
101 | 'DAYS360("1/31/2019", 3/31/2019)': -42870,
102 | 'DAYS360("3/15/2019", "3/31/2019")': 16,
103 | 'DAYS360("3/15/2019", "3/31/2020")': 376,
104 | 'DAYS360("12/31/2019", "1/9/2020")': 9,
105 | },
106 |
107 | EDATE: {
108 | 'EDATE("15-Jan-11",1)': 40589,
109 | },
110 |
111 | EOMONTH: {
112 | 'EOMONTH("1-Jan-11",1)': 40602,
113 | 'EOMONTH("1-Jan-11",-3)': 40482
114 | },
115 |
116 | HOUR: {
117 | 'HOUR(0.75)': 18,
118 | 'HOUR("7/18/2011 7:45")': 7,
119 | 'HOUR("4/21/2012")': 0,
120 | 'HOUR("4 PM")': 16,
121 | 'HOUR("4")': 0,
122 | 'HOUR("16:00")': 16,
123 | },
124 |
125 | ISOWEEKNUM: {
126 | 'ISOWEEKNUM("3/9/2012")': 10,
127 | },
128 |
129 | MINUTE: {
130 | 'MINUTE("12:45:00 PM")': 45,
131 | },
132 |
133 | MONTH: {
134 | 'MONTH("15-Apr-11")': 4
135 | },
136 |
137 | NETWORKDAYS: {
138 | 'NETWORKDAYS("10/1/2012","3/1/2013")': 110,
139 | 'NETWORKDAYS("10/1/2012","3/1/2013", 41235)': 109,
140 | 'NETWORKDAYS("10/1/2012","3/1/2013", {41235})': 109,
141 | 'NETWORKDAYS("10/1/2012","3/1/2013", A4)': 109,
142 | 'NETWORKDAYS("10/1/2012","3/1/2013", A4:A6)': 107,
143 | 'NETWORKDAYS(DATE(2006,1,1),DATE(2006,1,31))': 22,
144 | 'NETWORKDAYS(DATE(2006,2,28),DATE(2006,1,31))': -21,
145 | },
146 |
147 | 'NETWORKDAYS.INTL': {
148 | 'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,1,31))': 22,
149 | 'NETWORKDAYS.INTL(DATE(2006,2,28),DATE(2006,1,31))': -21,
150 | 'NETWORKDAYS.INTL(DATE(2006,2,28),DATE(2006,1,31), "1111111")': 0,
151 | 'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,2,1),7,{"2006/1/2","2006/1/16"})': 22,
152 | 'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,2,1),"0010001",{"2006/1/2","2006/1/16"})': 20,
153 | 'NETWORKDAYS.INTL(DATE(2006,1,1),DATE(2006,1,31), "1")': FormulaError.VALUE,
154 | 'NETWORKDAYS.INTL(DATE(2006,2,28),DATE(2006,1,31), "01111111")': FormulaError.VALUE,
155 | },
156 |
157 | NOW: {
158 | 'YEAR(NOW())': new Date().getFullYear(),
159 | // may have very low chance to fail, at the end of each hour.
160 | 'HOUR(NOW())': new Date().getHours(),
161 | },
162 |
163 | SECOND: {
164 | 'SECOND("4:48:18 PM")': 18,
165 | 'SECOND("4:48 PM")': 0,
166 | },
167 |
168 | TIME: {
169 | 'TIME(12,0,0)': 0.5,
170 | 'TIME(16,48,10)': 0.7001157407407408,
171 | 'TIME(-12,0,0)': FormulaError.NUM,
172 | },
173 |
174 | TIMEVALUE: {
175 | 'TIMEVALUE("2:24 AM")': 0.1,
176 | 'TIMEVALUE("22-Aug-2011 6:35 AM")': 0.2743055555555556,
177 | },
178 |
179 | TODAY: {
180 | 'YEAR(TODAY())': new Date().getFullYear(),
181 | },
182 |
183 | WEEKDAY: {
184 | 'WEEKDAY("2/14/2008")': 5,
185 | 'WEEKDAY("2/14/2008", 2)': 4,
186 | 'WEEKDAY("2/14/2008", "2")': 4,
187 | 'WEEKDAY("2/14/2008", 3)': 3,
188 | 'WEEKDAY("2/14/2008", 5)': FormulaError.NUM,
189 | },
190 |
191 | WEEKNUM: {
192 | 'WEEKNUM("3/9/2012")': 10,
193 | 'WEEKNUM("3/9/2012",2)': 11,
194 | 'WEEKNUM("3/9/2012",21)': 10,
195 | 'ISOWEEKNUM("3/9/2012")': 10,
196 | },
197 |
198 | WORKDAY: {
199 | 'WORKDAY(DATE(2008,10,1),1)': 39723,
200 | 'WORKDAY(DATE(2008,10,1),5)': 39729,
201 | 'WORKDAY(DATE(2008,10,9),1)': 39731,
202 | 'WORKDAY(DATE(2008,10,9),2)': 39734,
203 | 'WORKDAY(DATE(2008,10,1),151)': 39933,
204 | 'WORKDAY(DATE(2008,10,1),399)': 40281,
205 | 'WORKDAY(DATE(2008,10,1),151, {"2008/11/26","2008/12/4","2008/1/21"})': 39937,
206 | },
207 |
208 | 'WORKDAY.INTL': {
209 | 'WORKDAY.INTL(DATE(2012,1,1),30,0)': FormulaError.NUM,
210 | 'WORKDAY.INTL(DATE(2012,1,1),30,"1")': FormulaError.VALUE,
211 | 'WORKDAY.INTL(DATE(2012,1,1),1,11)': 40910,
212 | 'WORKDAY.INTL(DATE(2012,1,1),1,"0000011")': 40910,
213 | 'WORKDAY.INTL(DATE(2012,1,1),1,"1111111")': FormulaError.VALUE,
214 | 'WORKDAY.INTL(DATE(2012,1,1),1,"011111")': FormulaError.VALUE,
215 | 'WORKDAY.INTL(DATE(2012,1,1),90,11)': 41013,
216 | 'WORKDAY.INTL(DATE(2012,1,1),30,17)': 40944,
217 | 'TEXT(WORKDAY.INTL(DATE(2012,1,1),30,17),"m/dd/yyyy")': '2/05/2012'
218 | },
219 |
220 | YEAR: {
221 | 'YEAR("7/5/2008")': 2008,
222 | 'YEAR("7/5/2010")': 2010,
223 | },
224 |
225 | YEARFRAC: {
226 | 'YEARFRAC("2012/1/1","2012/7/30")': 0.58055556,
227 | 'YEARFRAC("1/1/2012","7/30/2012")': 0.58055556,
228 | 'YEARFRAC("2006/1/31","2006/3/31")': 0.1666666667,
229 | 'YEARFRAC("2006/1/31","2006/3/29")': 0.163888889,
230 | 'YEARFRAC("2006/1/30","2006/3/31")': 0.1666666667,
231 | 'YEARFRAC("1900/1/30","1900/3/31", 1)': 0.167123288,
232 | 'YEARFRAC("1900/3/31","1900/1/30", 1)': 0.167123288,
233 |
234 | 'YEARFRAC("2020/2/5","2021/2/1", 1)': 0.989071038,
235 | 'YEARFRAC("2020/2/1","2021/1/1", 1)': 0.915300546,
236 | 'YEARFRAC("2020/2/1","2022/1/1", 1)': 1.916058394,
237 | 'YEARFRAC("1/1/2006","3/1/2006", 1)': 0.161643836,
238 | 'YEARFRAC("1/1/2012","7/30/2012", 1)': 0.57650273,
239 | 'YEARFRAC("1/1/2012","7/30/2019", 1)': 7.575633128,
240 | 'YEARFRAC("1/1/2012","7/30/2012", 2)': 0.58611111,
241 | 'YEARFRAC("2012/1/1","2014/7/30", 2)': 2.613888889,
242 | 'YEARFRAC("1/1/2012","7/30/2012", 3)': 0.57808219,
243 | 'YEARFRAC("1/1/2012","7/30/2012", 4)': 0.58055556,
244 | 'YEARFRAC("2012/1/1","2013/7/30",4)': 1.580555556,
245 |
246 | 'YEARFRAC("2012/1/1","2012/7/30", 5)': FormulaError.VALUE,
247 | },
248 | };
249 |
--------------------------------------------------------------------------------
/grammar/dependency/utils.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../formulas/error');
2 | const {FormulaHelpers, Types, Address} = require('../../formulas/helpers');
3 | const {Prefix, Postfix, Infix, Operators} = require('../../formulas/operators');
4 | const Collection = require('../type/collection');
5 | const MAX_ROW = 1048576, MAX_COLUMN = 16384;
6 |
7 | class Utils {
8 |
9 | constructor(context) {
10 | this.context = context;
11 | }
12 |
13 | columnNameToNumber(columnName) {
14 | return Address.columnNameToNumber(columnName);
15 | }
16 |
17 | /**
18 | * Parse the cell address only.
19 | * @param {string} cellAddress
20 | * @return {{ref: {col: number, address: string, row: number}}}
21 | */
22 | parseCellAddress(cellAddress) {
23 | const res = cellAddress.match(/([$]?)([A-Za-z]{1,3})([$]?)([1-9][0-9]*)/);
24 | // console.log('parseCellAddress', cellAddress);
25 | return {
26 | ref: {
27 | col: this.columnNameToNumber(res[2]),
28 | row: +res[4]
29 | },
30 | };
31 | }
32 |
33 | parseRow(row) {
34 | const rowNum = +row;
35 | if (!Number.isInteger(rowNum))
36 | throw Error('Row number must be integer.');
37 | return {
38 | ref: {
39 | col: undefined,
40 | row: +row
41 | },
42 | };
43 | }
44 |
45 | parseCol(col) {
46 | return {
47 | ref: {
48 | col: this.columnNameToNumber(col),
49 | row: undefined,
50 | },
51 | };
52 | }
53 |
54 | /**
55 | * Apply + or - unary prefix.
56 | * @param {Array.} prefixes
57 | * @param {*} value
58 | * @return {*}
59 | */
60 | applyPrefix(prefixes, value) {
61 | this.extractRefValue(value);
62 | return 0;
63 | }
64 |
65 | applyPostfix(value, postfix) {
66 | this.extractRefValue(value);
67 | return 0
68 | }
69 |
70 | applyInfix(value1, infix, value2) {
71 | this.extractRefValue(value1);
72 | this.extractRefValue(value2);
73 | return 0;
74 | }
75 |
76 | applyIntersect(refs) {
77 | // console.log('applyIntersect', refs);
78 | if (this.isFormulaError(refs[0]))
79 | return refs[0];
80 | if (!refs[0].ref)
81 | throw Error(`Expecting a reference, but got ${refs[0]}.`);
82 | // a intersection will keep track of references, value won't be retrieved here.
83 | let maxRow, maxCol, minRow, minCol, sheet, res; // index start from 1
84 | // first time setup
85 | const ref = refs.shift().ref;
86 | sheet = ref.sheet;
87 | if (!ref.from) {
88 | // check whole row/col reference
89 | if (ref.row === undefined || ref.col === undefined) {
90 | throw Error('Cannot intersect the whole row or column.')
91 | }
92 |
93 | // cell ref
94 | maxRow = minRow = ref.row;
95 | maxCol = minCol = ref.col;
96 | } else {
97 | // range ref
98 | // update
99 | maxRow = Math.max(ref.from.row, ref.to.row);
100 | minRow = Math.min(ref.from.row, ref.to.row);
101 | maxCol = Math.max(ref.from.col, ref.to.col);
102 | minCol = Math.min(ref.from.col, ref.to.col);
103 | }
104 |
105 | let err;
106 | refs.forEach(ref => {
107 | if (this.isFormulaError(ref))
108 | return ref;
109 | ref = ref.ref;
110 | if (!ref) throw Error(`Expecting a reference, but got ${ref}.`);
111 | if (!ref.from) {
112 | if (ref.row === undefined || ref.col === undefined) {
113 | throw Error('Cannot intersect the whole row or column.')
114 | }
115 | // cell ref
116 | if (ref.row > maxRow || ref.row < minRow || ref.col > maxCol || ref.col < minCol
117 | || sheet !== ref.sheet) {
118 | err = FormulaError.NULL;
119 | }
120 | maxRow = minRow = ref.row;
121 | maxCol = minCol = ref.col;
122 | } else {
123 | // range ref
124 | const refMaxRow = Math.max(ref.from.row, ref.to.row);
125 | const refMinRow = Math.min(ref.from.row, ref.to.row);
126 | const refMaxCol = Math.max(ref.from.col, ref.to.col);
127 | const refMinCol = Math.min(ref.from.col, ref.to.col);
128 | if (refMinRow > maxRow || refMaxRow < minRow || refMinCol > maxCol || refMaxCol < minCol
129 | || sheet !== ref.sheet) {
130 | err = FormulaError.NULL;
131 | }
132 | // update
133 | maxRow = Math.min(maxRow, refMaxRow);
134 | minRow = Math.max(minRow, refMinRow);
135 | maxCol = Math.min(maxCol, refMaxCol);
136 | minCol = Math.max(minCol, refMinCol);
137 | }
138 | });
139 | if (err) return err;
140 | // check if the ref can be reduced to cell reference
141 | if (maxRow === minRow && maxCol === minCol) {
142 | res = {
143 | ref: {
144 | sheet,
145 | row: maxRow,
146 | col: maxCol
147 | }
148 | }
149 | } else {
150 | res = {
151 | ref: {
152 | sheet,
153 | from: {row: minRow, col: minCol},
154 | to: {row: maxRow, col: maxCol}
155 | }
156 | };
157 | }
158 |
159 | if (!res.ref.sheet)
160 | delete res.ref.sheet;
161 | return res;
162 | }
163 |
164 | applyUnion(refs) {
165 | const collection = new Collection();
166 | for (let i = 0; i < refs.length; i++) {
167 | if (this.isFormulaError(refs[i]))
168 | return refs[i];
169 | collection.add(this.extractRefValue(refs[i]).val, refs[i]);
170 | }
171 |
172 | // console.log('applyUnion', unions);
173 | return collection;
174 | }
175 |
176 | /**
177 | * Apply multiple references, e.g. A1:B3:C8:A:1:.....
178 | * @param refs
179 | // * @return {{ref: {from: {col: number, row: number}, to: {col: number, row: number}}}}
180 | */
181 | applyRange(refs) {
182 | let res, maxRow = -1, maxCol = -1, minRow = MAX_ROW + 1, minCol = MAX_COLUMN + 1;
183 | refs.forEach(ref => {
184 | if (this.isFormulaError(ref))
185 | return ref;
186 | // row ref is saved as number, parse the number to row ref here
187 | if (typeof ref === 'number') {
188 | ref = this.parseRow(ref);
189 | }
190 | ref = ref.ref;
191 | // check whole row/col reference
192 | if (ref.row === undefined) {
193 | minRow = 1;
194 | maxRow = MAX_ROW
195 | }
196 | if (ref.col === undefined) {
197 | minCol = 1;
198 | maxCol = MAX_COLUMN;
199 | }
200 |
201 | if (ref.row > maxRow)
202 | maxRow = ref.row;
203 | if (ref.row < minRow)
204 | minRow = ref.row;
205 | if (ref.col > maxCol)
206 | maxCol = ref.col;
207 | if (ref.col < minCol)
208 | minCol = ref.col;
209 | });
210 | if (maxRow === minRow && maxCol === minCol) {
211 | res = {
212 | ref: {
213 | row: maxRow,
214 | col: maxCol
215 | }
216 | }
217 | } else {
218 | res = {
219 | ref: {
220 | from: {row: minRow, col: minCol},
221 | to: {row: maxRow, col: maxCol}
222 | }
223 | };
224 | }
225 | return res;
226 | }
227 |
228 | /**
229 | * Throw away the refs, and retrieve the value.
230 | * @return {{val: *, isArray: boolean}}
231 | */
232 | extractRefValue(obj) {
233 | const isArray = Array.isArray(obj);
234 | if (obj.ref) {
235 | // can be number or array
236 | return {val: this.context.retrieveRef(obj), isArray};
237 |
238 | }
239 | return {val: obj, isArray};
240 | }
241 |
242 | /**
243 | *
244 | * @param array
245 | * @return {Array}
246 | */
247 | toArray(array) {
248 | // TODO: check if array is valid
249 | // console.log('toArray', array);
250 | return array;
251 | }
252 |
253 | /**
254 | * @param {string} number
255 | * @return {number}
256 | */
257 | toNumber(number) {
258 | return Number(number);
259 | }
260 |
261 | /**
262 | * @param {string} string
263 | * @return {string}
264 | */
265 | toString(string) {
266 | return string.substring(1, string.length - 1) .replace(/""/g, '"');
267 | }
268 |
269 | /**
270 | * @param {string} bool
271 | * @return {boolean}
272 | */
273 | toBoolean(bool) {
274 | return bool === 'TRUE';
275 | }
276 |
277 | /**
278 | * Parse an error.
279 | * @param {string} error
280 | * @return {FormulaError}
281 | */
282 | toError(error) {
283 | return new FormulaError(error.toUpperCase());
284 | }
285 |
286 | isFormulaError(obj) {
287 | return obj instanceof FormulaError;
288 | }
289 | }
290 |
291 | module.exports = Utils;
292 |
--------------------------------------------------------------------------------
/formulas/functions/text.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers, Types, WildCard} = require('../helpers');
3 | const H = FormulaHelpers;
4 |
5 | // Spreadsheet number format
6 | const ssf = require('../../ssf/ssf');
7 |
8 | // Change number to Thai pronunciation string
9 | const bahttext = require('bahttext');
10 |
11 | // full-width and half-width converter
12 | const charsets = {
13 | latin: {halfRE: /[!-~]/g, fullRE: /[!-~]/g, delta: 0xFEE0},
14 | hangul1: {halfRE: /[ᄀ-ᄒ]/g, fullRE: /[ᆨ-ᇂ]/g, delta: -0xEDF9},
15 | hangul2: {halfRE: /[ᅡ-ᅵ]/g, fullRE: /[ᅡ-ᅵ]/g, delta: -0xEE61},
16 | kana: {
17 | delta: 0,
18 | half: "。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚",
19 | full: "。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシ" +
20 | "スセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゛゜"
21 | },
22 | extras: {
23 | delta: 0,
24 | half: "¢£¬¯¦¥₩\u0020|←↑→↓■°",
25 | full: "¢£¬ ̄¦¥₩\u3000│←↑→↓■○"
26 | }
27 | };
28 | const toFull = set => c => set.delta ?
29 | String.fromCharCode(c.charCodeAt(0) + set.delta) :
30 | [...set.full][[...set.half].indexOf(c)];
31 | const toHalf = set => c => set.delta ?
32 | String.fromCharCode(c.charCodeAt(0) - set.delta) :
33 | [...set.half][[...set.full].indexOf(c)];
34 | const re = (set, way) => set[way + "RE"] || new RegExp("[" + set[way] + "]", "g");
35 | const sets = Object.keys(charsets).map(i => charsets[i]);
36 | const toFullWidth = str0 =>
37 | sets.reduce((str, set) => str.replace(re(set, "half"), toFull(set)), str0);
38 | const toHalfWidth = str0 =>
39 | sets.reduce((str, set) => str.replace(re(set, "full"), toHalf(set)), str0);
40 |
41 | const TextFunctions = {
42 | ASC: (text) => {
43 | text = H.accept(text, Types.STRING);
44 | return toHalfWidth(text);
45 | },
46 |
47 | BAHTTEXT: (number) => {
48 | number = H.accept(number, Types.NUMBER);
49 | try {
50 | return bahttext(number);
51 | } catch (e) {
52 | throw Error(`Error in https://github.com/jojoee/bahttext \n${e.toString()}`)
53 | }
54 | },
55 |
56 | CHAR: (number) => {
57 | number = H.accept(number, Types.NUMBER);
58 | if (number > 255 || number < 1)
59 | throw FormulaError.VALUE;
60 | return String.fromCharCode(number);
61 | },
62 |
63 | CLEAN: (text) => {
64 | text = H.accept(text, Types.STRING);
65 | return text.replace(/[\x00-\x1F]/g, '');
66 | },
67 |
68 | CODE: (text) => {
69 | text = H.accept(text, Types.STRING);
70 | if (text.length === 0)
71 | throw FormulaError.VALUE;
72 | return text.charCodeAt(0);
73 | },
74 |
75 | CONCAT: (...params) => {
76 | let text = '';
77 | // does not allow union
78 | H.flattenParams(params, Types.STRING, false, item => {
79 | item = H.accept(item, Types.STRING);
80 | text += item;
81 | });
82 | return text
83 | },
84 |
85 | CONCATENATE: (...params) => {
86 | let text = '';
87 | if (params.length === 0)
88 | throw Error('CONCATENATE need at least one argument.');
89 | params.forEach(param => {
90 | // does not allow range reference, array, union
91 | param = H.accept(param, Types.STRING);
92 | text += param;
93 | });
94 |
95 | return text;
96 | },
97 |
98 | DBCS: (text) => {
99 | text = H.accept(text, Types.STRING);
100 | return toFullWidth(text);
101 | },
102 |
103 | DOLLAR: (number, decimals) => {
104 | number = H.accept(number, Types.NUMBER);
105 | decimals = H.accept(decimals, Types.NUMBER, 2);
106 | const decimalString = Array(decimals).fill('0').join('');
107 | // Note: does not support locales
108 | // TODO: change currency based on user locale or settings from this library
109 | return ssf.format(`$#,##0.${decimalString}_);($#,##0.${decimalString})`, number).trim();
110 | },
111 |
112 | EXACT: (text1, text2) => {
113 | text1 = H.accept(text1, [Types.STRING]);
114 | text2 = H.accept(text2, [Types.STRING]);
115 |
116 | return text1 === text2;
117 | },
118 |
119 | FIND: (findText, withinText, startNum) => {
120 | findText = H.accept(findText, Types.STRING);
121 | withinText = H.accept(withinText, Types.STRING);
122 | startNum = H.accept(startNum, Types.NUMBER, 1);
123 | if (startNum < 1 || startNum > withinText.length)
124 | throw FormulaError.VALUE;
125 | const res = withinText.indexOf(findText, startNum - 1);
126 | if (res === -1)
127 | throw FormulaError.VALUE;
128 | return res + 1;
129 | },
130 |
131 | FINDB: (...params) => {
132 | return TextFunctions.FIND(...params);
133 | },
134 |
135 | FIXED: (number, decimals, noCommas) => {
136 | number = H.accept(number, Types.NUMBER);
137 | decimals = H.accept(decimals, Types.NUMBER, 2);
138 | noCommas = H.accept(noCommas, Types.BOOLEAN, false);
139 |
140 | const decimalString = Array(decimals).fill('0').join('');
141 | const comma = noCommas ? '' : '#,';
142 | return ssf.format(`${comma}##0.${decimalString}_);(${comma}##0.${decimalString})`, number).trim();
143 | },
144 |
145 | LEFT: (text, numChars) => {
146 | text = H.accept(text, Types.STRING);
147 | numChars = H.accept(numChars, Types.NUMBER, 1);
148 |
149 | if (numChars < 0)
150 | throw FormulaError.VALUE;
151 | if (numChars > text.length)
152 | return text;
153 | return text.slice(0, numChars);
154 | },
155 |
156 | LEFTB: (...params) => {
157 | return TextFunctions.LEFT(...params);
158 | },
159 |
160 | LEN: (text) => {
161 | text = H.accept(text, Types.STRING);
162 | return text.length;
163 | },
164 |
165 | LENB: (...params) => {
166 | return TextFunctions.LEN(...params);
167 | },
168 |
169 | LOWER: (text) => {
170 | text = H.accept(text, Types.STRING);
171 | return text.toLowerCase();
172 | },
173 |
174 | MID: (text, startNum, numChars) => {
175 | text = H.accept(text, Types.STRING);
176 | startNum = H.accept(startNum, Types.NUMBER);
177 | numChars = H.accept(numChars, Types.NUMBER);
178 | if (startNum > text.length)
179 | return '';
180 | if (startNum < 1 || numChars < 1)
181 | throw FormulaError.VALUE;
182 | return text.slice(startNum - 1, startNum + numChars - 1);
183 | },
184 |
185 | MIDB: (...params) => {
186 | return TextFunctions.MID(...params);
187 | },
188 |
189 | NUMBERVALUE: (text, decimalSeparator, groupSeparator) => {
190 | text = H.accept(text, Types.STRING);
191 | // TODO: support reading system locale and set separators
192 | decimalSeparator = H.accept(decimalSeparator, Types.STRING, '.');
193 | groupSeparator = H.accept(groupSeparator, Types.STRING, ',');
194 |
195 | if (text.length === 0)
196 | return 0;
197 | if (decimalSeparator.length === 0 || groupSeparator.length === 0)
198 | throw FormulaError.VALUE;
199 | decimalSeparator = decimalSeparator[0];
200 | groupSeparator = groupSeparator[0];
201 | if (decimalSeparator === groupSeparator
202 | || text.indexOf(decimalSeparator) < text.lastIndexOf(groupSeparator))
203 | throw FormulaError.VALUE;
204 |
205 | const res = text.replace(groupSeparator, '')
206 | .replace(decimalSeparator, '.')
207 | // remove chars that not related to number
208 | .replace(/[^\-0-9.%()]/g, '')
209 | .match(/([(-]*)([0-9]*[.]*[0-9]+)([)]?)([%]*)/);
210 | if (!res)
211 | throw FormulaError.VALUE;
212 | // ["-123456.78%%", "(-", "123456.78", ")", "%%"]
213 | const leftParenOrMinus = res[1].length, rightParen = res[3].length, percent = res[4].length;
214 | let number = Number(res[2]);
215 | if (leftParenOrMinus > 1 || leftParenOrMinus && !rightParen
216 | || !leftParenOrMinus && rightParen || isNaN(number))
217 | throw FormulaError.VALUE;
218 | number = number / 100 ** percent;
219 | return leftParenOrMinus ? -number : number;
220 | },
221 |
222 | PHONETIC: () => {
223 | },
224 |
225 | PROPER: (text) => {
226 | text = H.accept(text, [Types.STRING]);
227 | text = text.toLowerCase();
228 | text = text.charAt(0).toUpperCase() + text.slice(1);
229 | return text.replace(/(?:[^a-zA-Z])([a-zA-Z])/g,
230 | letter => letter.toUpperCase());
231 | },
232 |
233 | REPLACE: (old_text, start_num, num_chars, new_text) => {
234 | old_text = H.accept(old_text, [Types.STRING]);
235 | start_num = H.accept(start_num, [Types.NUMBER]);
236 | num_chars = H.accept(num_chars, [Types.NUMBER]);
237 | new_text = H.accept(new_text, [Types.STRING]);
238 |
239 | let arr = old_text.split("");
240 | arr.splice(start_num - 1, num_chars, new_text);
241 |
242 | return arr.join("");
243 | },
244 |
245 | REPLACEB: (...params) => {
246 | return TextFunctions.REPLACE(...params)
247 | },
248 |
249 | REPT: (text, number_times) => {
250 | text = H.accept(text, Types.STRING);
251 | number_times = H.accept(number_times, Types.NUMBER);
252 | let str = "";
253 |
254 | for (let i = 0; i < number_times; i++) {
255 | str += text;
256 | }
257 | return str;
258 | },
259 |
260 | RIGHT: (text, numChars) => {
261 | text = H.accept(text, Types.STRING);
262 | numChars = H.accept(numChars, Types.NUMBER, 1);
263 |
264 | if (numChars < 0)
265 | throw FormulaError.VALUE;
266 | const len = text.length;
267 | if (numChars > len)
268 | return text;
269 | return text.slice(len - numChars);
270 | },
271 |
272 | RIGHTB: (...params) => {
273 | return TextFunctions.RIGHT(...params);
274 | },
275 |
276 | SEARCH: (findText, withinText, startNum) => {
277 | findText = H.accept(findText, Types.STRING);
278 | withinText = H.accept(withinText, Types.STRING);
279 | startNum = H.accept(startNum, Types.NUMBER, 1);
280 | if (startNum < 1 || startNum > withinText.length)
281 | throw FormulaError.VALUE;
282 |
283 | // transform to js regex expression
284 | let findTextRegex = WildCard.isWildCard(findText) ? WildCard.toRegex(findText, 'i') : findText;
285 | const res = withinText.slice(startNum - 1).search(findTextRegex);
286 | if (res === -1)
287 | throw FormulaError.VALUE;
288 | return res + startNum;
289 | },
290 |
291 | SEARCHB: (...params) => {
292 | return TextFunctions.SEARCH(...params)
293 | },
294 |
295 | SUBSTITUTE: (...params) => {
296 |
297 | },
298 |
299 | T: (value) => {
300 | // extract the real parameter
301 | value = H.accept(value);
302 | if (typeof value === "string")
303 | return value;
304 | return '';
305 | },
306 |
307 | TEXT: (value, formatText) => {
308 | value = H.accept(value, Types.NUMBER);
309 | formatText = H.accept(formatText, Types.STRING);
310 | // I know ssf contains bugs...
311 | try {
312 | return ssf.format(formatText, value);
313 | } catch (e) {
314 | console.error(e)
315 | throw FormulaError.VALUE;
316 | }
317 | },
318 |
319 | TEXTJOIN: (...params) => {
320 |
321 | },
322 |
323 | TRIM: (text) => {
324 | text = H.accept(text, [Types.STRING]);
325 | return text.replace(/^\s+|\s+$/g, '')
326 | },
327 |
328 | UNICHAR: (number) => {
329 | number = H.accept(number, [Types.NUMBER]);
330 | if (number <= 0)
331 | throw FormulaError.VALUE;
332 | return String.fromCharCode(number);
333 | },
334 |
335 | UNICODE: (text) => {
336 | return TextFunctions.CODE(text);
337 | },
338 |
339 | UPPER: (text) => {
340 | text = H.accept(text, Types.STRING);
341 | return text.toUpperCase();
342 | },
343 | };
344 |
345 | module.exports = TextFunctions;
346 |
--------------------------------------------------------------------------------
/test/formulas/math/testcase.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../../../formulas/error');
2 | module.exports = {
3 |
4 | ABS: {
5 | 'ABS(-1)': 1,
6 | },
7 |
8 | ARABIC: {
9 | 'ARABIC("LVII")': 57,
10 | 'ARABIC("")': 0,
11 | 'ARABIC("LVIIA")': FormulaError.VALUE,
12 | },
13 |
14 | BASE: {
15 | 'BASE(7,2)': '111',
16 | 'BASE(100,16)': '64',
17 | 'BASE(15,2,10)': '0000001111',
18 | 'BASE(2^53-1,36)': '2GOSA7PA2GV',
19 |
20 | 'BASE(-1,2)': FormulaError.NUM,
21 | 'BASE(2^53-1,2)': '11111111111111111111111111111111111111111111111111111',
22 | 'BASE(2^53,2)': FormulaError.NUM,
23 |
24 | 'BASE(7,1)': FormulaError.NUM,
25 | 'BASE(7,37)': FormulaError.NUM,
26 |
27 | 'BASE(7,2,-1)': FormulaError.NUM,
28 | 'BASE(7,2,0)': '111',
29 | 'BASE(7,2,2)': '111',
30 | 'BASE(7,2,5)': '00111',
31 | },
32 |
33 | CEILING: {
34 | 'CEILING(2.5, 1)': 3,
35 | 'CEILING(-2.5, -2)': -4,
36 | 'CEILING(-2.5, 2)': -2,
37 | 'CEILING(1.5, 0.1)': 1.5,
38 | 'CEILING(0.234, 0.01)': 0.24,
39 | 'CEILING(1.5, 0)': 0,
40 | 'CEILING(2^1024, 1)': FormulaError.NUM,
41 | },
42 |
43 | 'CEILING.MATH': {
44 | 'CEILING.MATH(24.3,5)': 25,
45 | 'CEILING.MATH(6.7)': 7,
46 | 'CEILING.MATH(-6.7)': -7,
47 | 'CEILING.MATH(-8.1,2)': -8,
48 | 'CEILING.MATH(-5.5,2,-1)': -6
49 | },
50 |
51 | 'CEILING.PRECISE': {
52 | 'CEILING.PRECISE(4.3)': 5,
53 | 'CEILING.PRECISE(-4.3)': -4,
54 | 'CEILING.PRECISE(4.3, 2)': 6,
55 | 'CEILING.PRECISE(4.3,-2)': 6,
56 | 'CEILING.PRECISE(-4.3,2)': -4,
57 | 'CEILING.PRECISE(-4.3,-2)': -4,
58 | },
59 |
60 | COMBINE: {
61 | 'COMBIN(8,2)': 28,
62 | 'COMBIN(-1,2)': FormulaError.NUM,
63 | 'COMBIN(1,2)': FormulaError.NUM,
64 | 'COMBIN(1,-2)': FormulaError.NUM,
65 | },
66 |
67 | COMBINA: {
68 | 'COMBINA(4,3)': 20,
69 | 'COMBINA(0,0)': 1,
70 | 'COMBINA(1,0)': 1,
71 | 'COMBINA(-1,2)': FormulaError.NUM,
72 | 'COMBINA(1,2)': 1,
73 | 'COMBINA(1,-2)': FormulaError.NUM,
74 | },
75 |
76 | DECIMAL: {
77 | 'DECIMAL("FF",16)': 255,
78 | 'DECIMAL("8000000000",16)': 549755813888,
79 | 'DECIMAL(111,2)': 7,
80 | 'DECIMAL("zap",36)': 45745,
81 | 'DECIMAL("zap",2)': FormulaError.NUM,
82 | 'DECIMAL("zap",37)': FormulaError.NUM,
83 | 'DECIMAL("zap",1)': FormulaError.NUM,
84 | },
85 |
86 | EVEN: {
87 | 'EVEN(1.5)': 2,
88 | 'EVEN(3)': 4,
89 | 'EVEN(2)': 2,
90 | 'EVEN(-1)': -2,
91 | },
92 |
93 | EXP: {
94 | 'EXP(1)': 2.71828183
95 | },
96 |
97 | FACT: {
98 | 'FACT(5)': 120,
99 | 'FACT(150)': 5.7133839564458575e+262, // more accurate than excel...
100 | 'FACT(150) + 1': 5.7133839564458575e+262 + 1, // memorization
101 | 'FACT(1.9)': 1,
102 | 'FACT(0)': 1,
103 | 'FACT(-1)': FormulaError.NUM,
104 | 'FACT(1)': 1,
105 | },
106 |
107 | FACTDOUBLE: {
108 | 'FACTDOUBLE(6)': 48,
109 | 'FACTDOUBLE(6) + 1': 49, // memorization
110 | 'FACTDOUBLE(7)': 105,
111 | 'FACTDOUBLE(0)': 1,
112 | 'FACTDOUBLE(-1)': 1,
113 | 'FACTDOUBLE(-2)': FormulaError.NUM,
114 | 'FACTDOUBLE(1)': 1,
115 | },
116 |
117 | FLOOR: {
118 | 'FLOOR(0,)': 0,
119 | 'FLOOR(12,0)': 0,
120 | 'FLOOR(3.7,2)': 2,
121 | 'FLOOR(-2.5,-2)': -2,
122 | 'FLOOR(-2.5,2)': -4,
123 | 'FLOOR(2.5,-2)': FormulaError.NUM,
124 | 'FLOOR(1.58,0.1)': 1.5,
125 | 'FLOOR(0.234,0.01)': 0.23,
126 | 'FLOOR(-8.1,2)': -10,
127 | },
128 |
129 | 'FLOOR.MATH': {
130 | 'FLOOR.MATH(0)': 0,
131 | 'FLOOR.MATH(12, 0)': 0,
132 | 'FLOOR.MATH(24.3,5)': 20,
133 | 'FLOOR.MATH(6.7)': 6,
134 | 'FLOOR.MATH(-8.1,2)': -10,
135 | 'FLOOR.MATH(-5.5,2,-1)': -4,
136 | 'FLOOR.MATH(-5.5,2,1)': -4,
137 | 'FLOOR.MATH(-5.5,2,)': -6,
138 | 'FLOOR.MATH(-5.5,2)': -6,
139 | 'FLOOR.MATH(-5.5,-2)': -6,
140 | 'FLOOR.MATH(5.5,2)': 4,
141 | 'FLOOR.MATH(5.5,-2)': 4,
142 | 'FLOOR.MATH(24.3,-5)': 20,
143 | 'FLOOR.MATH(-8.1,-2)': -10,
144 | },
145 |
146 | 'FLOOR.PRECISE': {
147 | 'FLOOR.PRECISE(-3.2,-1)': -4,
148 | 'FLOOR.PRECISE(3.2, 1)': 3,
149 | 'FLOOR.PRECISE(-3.2, 1)': -4,
150 | 'FLOOR.PRECISE(3.2,-1)': 3,
151 | 'FLOOR.PRECISE(3.2)': 3,
152 | 'FLOOR.PRECISE(0)': 0,
153 | 'FLOOR.PRECISE(3.2, 0)': 0,
154 | },
155 |
156 | GCD: {
157 | 'GCD(5, 2)': 1,
158 | 'GCD(24, 36)': 12,
159 | 'GCD(7, 1)': 1,
160 | 'GCD(5, 0)': 5,
161 | 'GCD(123, 0)': 123,
162 | 'GCD(128, 80, 44)': 4,
163 | 'GCD(128, 80, 44,)': 4,
164 | 'GCD(128, 80, 44, 2 ^ 53)': FormulaError.NUM, // excel parse this as #NUM!
165 | 'GCD("a")': FormulaError.VALUE,
166 | 'GCD(5, 2, (A1))': 1,
167 | 'GCD(5, 2, A1:E1)': 1,
168 | 'GCD(5, 2, (A1:E1))': 1,
169 | 'GCD(5, 2, (A1, A2))': FormulaError.VALUE, // does not support union
170 | 'GCD(5, 2, {3, 7})': 1,
171 | 'GCD(5, 2, {3, "7"})': 1,
172 | 'GCD(5, 2, {3, "7a"})': FormulaError.VALUE,
173 | 'GCD(5, 2, {3, "7"}, TRUE)': FormulaError.VALUE,
174 | },
175 |
176 | INT: {
177 | 'INT(0)': 0,
178 | 'INT(8.9)': 8,
179 | 'INT(-8.9)': -9,
180 | },
181 |
182 | 'ISO.CEILING': {
183 | 'ISO.CEILING(4.3)': 5,
184 | 'ISO.CEILING(-4.3)': -4,
185 | 'ISO.CEILING(4.3, 2)': 6,
186 | 'ISO.CEILING(4.3,-2)': 6,
187 | 'ISO.CEILING(-4.3,2)': -4,
188 | 'ISO.CEILING(-4.3,-2)': -4,
189 | },
190 |
191 | LCM: {
192 | 'LCM("a")': FormulaError.VALUE,
193 | 'LCM(5, 2)': 10,
194 | 'LCM(24, 36)': 72,
195 | 'LCM(50,56,100)': 1400,
196 | 'LCM(50,56,100,)': 1400,
197 | 'LCM(128, 80, 44, 2 ^ 53)': FormulaError.NUM, // excel parse this as #NUM!
198 | 'LCM(5, 2, (A1))': 10,
199 | 'LCM(5, 2, A1:E1)': 60,
200 | 'LCM(5, 2, (A1:E1))': 60,
201 | 'LCM(5, 2, (A1, A2))': FormulaError.VALUE, // does not support union
202 | 'LCM(5, 2, {3, 7})': 210,
203 | 'LCM(5, 2, {3, "7"})': 210,
204 | 'LCM(5, 2, {3, "7a"})': FormulaError.VALUE,
205 | 'LCM(5, 2, {3, "7"}, TRUE)': FormulaError.VALUE,
206 | },
207 |
208 | LN: {
209 | 'LN(86)': 4.454347296253507,
210 | 'LN(EXP(1))': 1,
211 | 'LN(EXP(3))': 3,
212 | },
213 |
214 | LOG: {
215 | 'LOG(10)': 1,
216 | 'LOG(8, 2)': 3,
217 | 'LOG(86, EXP(1))': 4.454347296253507,
218 | },
219 |
220 | LOG10: {
221 | 'LOG10(86)': 1.9344984512435677,
222 | 'LOG10(10)': 1,
223 | 'LOG10(100000)': 5,
224 | 'LOG10(10^5)': 5,
225 | },
226 |
227 | MDETERM: {
228 | 'MDETERM({3,6,1;1,1,0;3,10,2})': 1,
229 | 'MDETERM({3,6;1,1})': -3,
230 | 'MDETERM({6})': 6,
231 | 'MDETERM({1,3,8,5;1,3,6,1})': FormulaError.VALUE
232 | },
233 |
234 | MMULT: {
235 | 'MMULT({1,3;7,2}, {2,0;0,2})': 2,
236 | 'MMULT({1,3;7,2;1,1}, {2,0;0,2})': 2,
237 | 'MMULT({1,3;"r",2}, {2,0;0,2})': FormulaError.VALUE,
238 | 'MMULT({1,3;7,2}, {2,0;"0",2})': FormulaError.VALUE,
239 | },
240 |
241 | MOD: {
242 | 'MOD(3, 2)': 1,
243 | 'MOD(-3, 2)': 1,
244 | 'MOD(3, -2)': -1,
245 | 'MOD(-3, -2)': -1,
246 | 'MOD(-3, 0)': FormulaError.DIV0
247 | },
248 |
249 | MROUND: {
250 | 'MROUND(10, 1)': 10,
251 | 'MROUND(10, 3)': 9,
252 | 'MROUND(10, 0)': 0,
253 | 'MROUND(-10, -3)': -9,
254 | 'MROUND(1.3, 0.2)': 1.4,
255 | 'MROUND(5, -2)': FormulaError.NUM,
256 | 'MROUND(6.05,0.1)': 6.0, // same as excel, differ from google sheets
257 | 'MROUND(7.05,0.1)': 7.1,
258 | },
259 |
260 | MULTINOMIAL: {
261 | 'MULTINOMIAL({1,2}, E1, A1:D1)': 92626934400,
262 | 'MULTINOMIAL(2, 3, 4)': 1260,
263 | 'MULTINOMIAL(2, 3, -4)': FormulaError.NUM,
264 | },
265 |
266 | MUNIT: {
267 | 'MUNIT(1)': 1,
268 | 'MUNIT(10)': 1,
269 | },
270 |
271 | ODD: {
272 | 'ODD(0)': 1,
273 | 'ODD(1.5)': 3,
274 | 'ODD(3)': 3,
275 | 'ODD(2)': 3,
276 | 'ODD(-1)': -1,
277 | 'ODD(-2)': -3,
278 | },
279 |
280 | PI: {
281 | 'PI()': 3.14159265357989
282 | },
283 |
284 | POWER: {
285 | 'POWER(5,2)': 25,
286 | 'POWER(98.6,3.2)': 2401077.22206958000,
287 | 'POWER(4,5/4)': 5.656854249,
288 | },
289 |
290 | PRODUCT: {
291 | 'PRODUCT(1,2,3,4,5)': 120,
292 | 'PRODUCT(1,2,3,4,5, "2")': 240,
293 | 'PRODUCT(1,2,3,4,5, "2c")': 120,
294 | 'PRODUCT(A1:E1)': 120,
295 | 'PRODUCT((A1, B1:E1))': 120,
296 | 'PRODUCT(1,2,3,4,5, A1, {1,2})': 240,
297 | },
298 |
299 | QUOTIENT: {
300 | 'QUOTIENT(5, 2)': 2,
301 | 'QUOTIENT(4.5, 3.1)': 1,
302 | 'QUOTIENT(-10, 3)' : -3,
303 | 'QUOTIENT(-10, -3)': 3,
304 | },
305 |
306 | RADIANS: {
307 | 'RADIANS(270)': 4.71238898,
308 | 'RADIANS(0)': 0,
309 | },
310 |
311 | RAND: {
312 | 'RAND() > 0': true,
313 | },
314 |
315 | RANDBETWEEN: {
316 | 'RANDBETWEEN(-1,1) >= -1': true,
317 | },
318 |
319 | ROMAN: {
320 | 'ROMAN(499,0)': 'CDXCIX',
321 | },
322 |
323 | ROUND: {
324 | 'ROUND(2.15, 0)': 2,
325 | 'ROUND(2.15, 1)': 2.2,
326 | 'ROUND(2.149, 1)': 2.1,
327 | 'ROUND(-1.475, 2)': -1.48,
328 | 'ROUND(21.5, -1)': 20,
329 | 'ROUND(626.3,-3)': 1000,
330 | 'ROUND(1.98, -1)': 0,
331 | 'ROUND(-50.55,-2)': -100,
332 | },
333 |
334 | ROUNDDOWN: {
335 | 'ROUNDDOWN(3.2, 0)': 3,
336 | 'ROUNDDOWN(76.9,0)': 76,
337 | 'ROUNDDOWN(3.14159, 3)': 3.141,
338 | 'ROUNDDOWN(-3.14159, 1)': -3.1,
339 | 'ROUNDDOWN(31415.92654, -2)': 31400
340 | },
341 |
342 | ROUNDUP: {
343 | 'ROUNDUP(3.2,0)': 4,
344 | 'ROUNDUP(76.9,0)': 77,
345 | 'ROUNDUP(3.14159, 3)': 3.142,
346 | 'ROUNDUP(-3.14159, 1)': -3.2,
347 | 'ROUNDUP(31415.92654, -2)': 31500,
348 | },
349 |
350 | SERIESSUM: {
351 | 'SERIESSUM(PI()/4,0,2,{1, -0.5, 0.041666667, -0.001388889})': 0.707103215,
352 | 'SERIESSUM(PI()/4,0,2,{1, -0.5, 0.041666667, "12"})': FormulaError.VALUE,
353 | },
354 |
355 | SIGN: {
356 | 'SIGN(10)': 1,
357 | 'SIGN(4-4)': 0,
358 | 'SIGN(-0.00001)': -1,
359 | },
360 |
361 | SQRT: {
362 | 'SQRT(16)': 4,
363 | 'SQRT(-16)': FormulaError.NUM,
364 | 'SQRT(ABS(-16))': 4,
365 | },
366 |
367 | SQRTPI: {
368 | 'SQRTPI(1)': 1.772453851,
369 | 'SQRTPI(2)': 2.506628275,
370 | 'SQRTPI(-1)': FormulaError.NUM,
371 | },
372 | // TODO: Start from here.
373 |
374 |
375 | SUM: {
376 | 'SUM(1,2,3)': 6,
377 | 'SUM(A1:C1, C1:E1)': 18,
378 | 'SUM((A1:C1, C1:E1))': 18,
379 | 'SUM((A1:C1, C1:E1), A1)': 19,
380 | 'SUM((A1:C1, C1:E1), A13)': 18,
381 | 'SUM("1", {1})': 2,
382 | 'SUM("1", {"1"})': 1,
383 | 'SUM("1", {"1"},)': 1,
384 | 'SUM("1", {"1"},TRUE)': 2,
385 | },
386 |
387 | SUMIF: {
388 | 'SUMIF(A1:E1, ">1")': 14,
389 | 'SUMIF(A2:A5,">160000",B2:B5)': 63000,
390 | 'SUMIF(A2:A5,">160000")': 900000,
391 | 'SUMIF(A2:A5,300000,B2:B5)': 21000,
392 | 'SUMIF(A2:A5,">" & C2,B2:B5)': 49000,
393 | 'SUMIF(A7:A12,"Fruits",C7:C12)': 2000,
394 | 'SUMIF(A7:A12,"Vegetables",C7:C12)': 12000,
395 | 'SUMIF(B7:B12,"*es",C7:C12)': 4300,
396 | 'SUMIF(A7:A12,"",C7:C12)': 400,
397 | //The sum_range argument does not have to be the same size and shape as the range argument.
398 | // The actual cells that are added are determined by using the upper leftmost cell in the
399 | // sum_range argument as the beginning cell, and then including cells that correspond in size
400 | // and shape to the range argument. For example:
401 | 'SUMIF(A7:A12,"",C7)': 400,
402 | },
403 |
404 | SUMPRODUCT: {
405 | 'SUMPRODUCT({1,"12";7,2}, {2,1;5,2})': 41,
406 | 'SUMPRODUCT({1,12;7,2}, {2,1;5,2})': 53,
407 | 'SUMPRODUCT({1,12;7,2}, {2,1;5,"2"})': 49,
408 | 'SUMPRODUCT({1,12;7,2}, {2,1;5,2;1,1})': FormulaError.VALUE
409 | },
410 |
411 | SUMSQ: {
412 | 'SUMSQ(3, 4)': 25,
413 | 'SUMSQ(3, 4, A1)': 26,
414 | 'SUMSQ(3, 4, A1, A13)': 26,
415 | },
416 |
417 | SUMX2MY2: {
418 | 'SUMX2MY2(A14:G14,A15:G15)': -55,
419 | 'SUMX2MY2({2, 3, 9, 1, 8, 7, 5}, {6, 5, 11, 7, 5, 4, 4})': -55,
420 | 'SUMX2MY2({"2",3,9,1,8,7,5}, {6,5,11,7,5,4,4})': -23,
421 | 'SUMX2MY2(A14:G13,A15:G15)': FormulaError.NA,
422 | },
423 |
424 | SUMX2PY2: {
425 | 'SUMX2PY2(A14:G14,A15:G15)': 521,
426 | 'SUMX2PY2({2,3,9,1,8,7,5}, {6,5,11,7,5,4,4})': 521,
427 | 'SUMX2PY2({"2",3,9,1,8,7,5}, {6,5,11,7,5,4,4})': 481,
428 | 'SUMX2PY2(A14:G13,A15:G15)': FormulaError.NA,
429 | },
430 |
431 | SUMXMY2: {
432 | 'SUMXMY2(A14:G14,A15:G15)': 79,
433 | 'SUMXMY2({2,3,9,1,8,7,5}, {6,5,11,7,5,4,4})': 79,
434 | 'SUMXMY2({"2",3,9,1,8,7,5}, {6,5,11,7,5,4,4})': 63,
435 | 'SUMXMY2(A14:G13,A15:G15)': FormulaError.NA,
436 | },
437 |
438 | TRUNC: {
439 | 'TRUNC(8.9)': 8,
440 | 'TRUNC(-8.9)': -8,
441 | 'TRUNC(0.45)': 0,
442 | }
443 | };
444 |
--------------------------------------------------------------------------------
/grammar/utils.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../formulas/error');
2 | const {Address} = require('../formulas/helpers');
3 | const {Prefix, Postfix, Infix, Operators} = require('../formulas/operators');
4 | const Collection = require('./type/collection');
5 | const MAX_ROW = 1048576, MAX_COLUMN = 16384;
6 | const {NotAllInputParsedException} = require('chevrotain');
7 |
8 | class Utils {
9 |
10 | constructor(context) {
11 | this.context = context;
12 | }
13 |
14 | columnNameToNumber(columnName) {
15 | return Address.columnNameToNumber(columnName);
16 | }
17 |
18 | /**
19 | * Parse the cell address only.
20 | * @param {string} cellAddress
21 | * @return {{ref: {col: number, address: string, row: number}}}
22 | */
23 | parseCellAddress(cellAddress) {
24 | const res = cellAddress.match(/([$]?)([A-Za-z]{1,3})([$]?)([1-9][0-9]*)/);
25 | // console.log('parseCellAddress', cellAddress);
26 | return {
27 | ref: {
28 | address: res[0],
29 | col: this.columnNameToNumber(res[2]),
30 | row: +res[4]
31 | },
32 | };
33 | }
34 |
35 | parseRow(row) {
36 | const rowNum = +row;
37 | if (!Number.isInteger(rowNum))
38 | throw Error('Row number must be integer.');
39 | return {
40 | ref: {
41 | col: undefined,
42 | row: +row
43 | },
44 | };
45 | }
46 |
47 | parseCol(col) {
48 | return {
49 | ref: {
50 | col: this.columnNameToNumber(col),
51 | row: undefined,
52 | },
53 | };
54 | }
55 |
56 | parseColRange(col1, col2) {
57 | // const res = colRange.match(/([$]?)([A-Za-z]{1,3}):([$]?)([A-Za-z]{1,4})/);
58 | col1 = this.columnNameToNumber(col1);
59 | col2 = this.columnNameToNumber(col2);
60 | return {
61 | ref: {
62 | from: {
63 | col: Math.min(col1, col2),
64 | row: null
65 | },
66 | to: {
67 | col: Math.max(col1, col2),
68 | row: null
69 | }
70 | }
71 | }
72 | }
73 |
74 | parseRowRange(row1, row2) {
75 | // const res = rowRange.match(/([$]?)([1-9][0-9]*):([$]?)([1-9][0-9]*)/);
76 | return {
77 | ref: {
78 | from: {
79 | col: null,
80 | row: Math.min(row1, row2),
81 | },
82 | to: {
83 | col: null,
84 | row: Math.max(row1, row2),
85 | }
86 | }
87 |
88 | }
89 | }
90 |
91 |
92 | _applyPrefix(prefixes, val, isArray) {
93 | if (this.isFormulaError(val))
94 | return val;
95 | return Prefix.unaryOp(prefixes, val, isArray);
96 | }
97 |
98 | async applyPrefixAsync(prefixes, value) {
99 | const {val, isArray} = this.extractRefValue(await value);
100 | return this._applyPrefix(prefixes, val, isArray);
101 | }
102 |
103 | /**
104 | * Apply + or - unary prefix.
105 | * @param {Array.} prefixes
106 | * @param {*} value
107 | * @return {*}
108 | */
109 | applyPrefix(prefixes, value) {
110 | // console.log('applyPrefix', prefixes, value);
111 | if (this.context.async) {
112 | return this.applyPrefixAsync(prefixes, value);
113 | } else {
114 | const {val, isArray} = this.extractRefValue(value);
115 | return this._applyPrefix(prefixes, val, isArray);
116 | }
117 | }
118 |
119 | _applyPostfix(val, isArray, postfix) {
120 | if (this.isFormulaError(val))
121 | return val;
122 | return Postfix.percentOp(val, postfix, isArray);
123 | }
124 |
125 | async applyPostfixAsync(value, postfix) {
126 | const {val, isArray} = this.extractRefValue(await value);
127 | return this._applyPostfix(val, isArray, postfix);
128 | }
129 |
130 | applyPostfix(value, postfix) {
131 | // console.log('applyPostfix', value, postfix);
132 | if (this.context.async) {
133 | return this.applyPostfixAsync(value, postfix);
134 | } else {
135 | const {val, isArray} = this.extractRefValue(value);
136 | return this._applyPostfix(val, isArray, postfix)
137 | }
138 | }
139 |
140 | _applyInfix(res1, infix, res2) {
141 | const val1 = res1.val, isArray1 = res1.isArray;
142 | const val2 = res2.val, isArray2 = res2.isArray;
143 | if (this.isFormulaError(val1))
144 | return val1;
145 | if (this.isFormulaError(val2))
146 | return val2;
147 | if (Operators.compareOp.includes(infix))
148 | return Infix.compareOp(val1, infix, val2, isArray1, isArray2);
149 | else if (Operators.concatOp.includes(infix))
150 | return Infix.concatOp(val1, infix, val2, isArray1, isArray2);
151 | else if (Operators.mathOp.includes(infix))
152 | return Infix.mathOp(val1, infix, val2, isArray1, isArray2);
153 | else
154 | throw new Error(`Unrecognized infix: ${infix}`);
155 | }
156 |
157 | async applyInfixAsync(value1, infix, value2) {
158 | const res1 = this.extractRefValue(await value1);
159 | const res2 = this.extractRefValue(await value2);
160 | return this._applyInfix(res1, infix, res2)
161 | }
162 |
163 | applyInfix(value1, infix, value2) {
164 | if (this.context.async) {
165 | return this.applyInfixAsync(value1, infix, value2)
166 | } else {
167 | const res1 = this.extractRefValue(value1);
168 | const res2 = this.extractRefValue(value2);
169 | return this._applyInfix(res1, infix, res2)
170 | }
171 | }
172 |
173 | applyIntersect(refs) {
174 | // console.log('applyIntersect', refs);
175 | if (this.isFormulaError(refs[0]))
176 | return refs[0];
177 | if (!refs[0].ref)
178 | throw Error(`Expecting a reference, but got ${refs[0]}.`);
179 | // a intersection will keep track of references, value won't be retrieved here.
180 | let maxRow, maxCol, minRow, minCol, sheet, res; // index start from 1
181 | // first time setup
182 | const ref = refs.shift().ref;
183 | sheet = ref.sheet;
184 | if (!ref.from) {
185 | // check whole row/col reference
186 | if (ref.row === undefined || ref.col === undefined) {
187 | throw Error('Cannot intersect the whole row or column.')
188 | }
189 |
190 | // cell ref
191 | maxRow = minRow = ref.row;
192 | maxCol = minCol = ref.col;
193 | } else {
194 | // range ref
195 | // update
196 | maxRow = Math.max(ref.from.row, ref.to.row);
197 | minRow = Math.min(ref.from.row, ref.to.row);
198 | maxCol = Math.max(ref.from.col, ref.to.col);
199 | minCol = Math.min(ref.from.col, ref.to.col);
200 | }
201 |
202 | let err;
203 | refs.forEach(ref => {
204 | if (this.isFormulaError(ref))
205 | return ref;
206 | ref = ref.ref;
207 | if (!ref) throw Error(`Expecting a reference, but got ${ref}.`);
208 | if (!ref.from) {
209 | if (ref.row === undefined || ref.col === undefined) {
210 | throw Error('Cannot intersect the whole row or column.')
211 | }
212 | // cell ref
213 | if (ref.row > maxRow || ref.row < minRow || ref.col > maxCol || ref.col < minCol
214 | || sheet !== ref.sheet) {
215 | err = FormulaError.NULL;
216 | }
217 | maxRow = minRow = ref.row;
218 | maxCol = minCol = ref.col;
219 | } else {
220 | // range ref
221 | const refMaxRow = Math.max(ref.from.row, ref.to.row);
222 | const refMinRow = Math.min(ref.from.row, ref.to.row);
223 | const refMaxCol = Math.max(ref.from.col, ref.to.col);
224 | const refMinCol = Math.min(ref.from.col, ref.to.col);
225 | if (refMinRow > maxRow || refMaxRow < minRow || refMinCol > maxCol || refMaxCol < minCol
226 | || sheet !== ref.sheet) {
227 | err = FormulaError.NULL;
228 | }
229 | // update
230 | maxRow = Math.min(maxRow, refMaxRow);
231 | minRow = Math.max(minRow, refMinRow);
232 | maxCol = Math.min(maxCol, refMaxCol);
233 | minCol = Math.max(minCol, refMinCol);
234 | }
235 | });
236 | if (err) return err;
237 | // check if the ref can be reduced to cell reference
238 | if (maxRow === minRow && maxCol === minCol) {
239 | res = {
240 | ref: {
241 | sheet,
242 | row: maxRow,
243 | col: maxCol
244 | }
245 | }
246 | } else {
247 | res = {
248 | ref: {
249 | sheet,
250 | from: {row: minRow, col: minCol},
251 | to: {row: maxRow, col: maxCol}
252 | }
253 | };
254 | }
255 |
256 | if (!res.ref.sheet)
257 | delete res.ref.sheet;
258 | return res;
259 | }
260 |
261 | applyUnion(refs) {
262 | const collection = new Collection();
263 | for (let i = 0; i < refs.length; i++) {
264 | if (this.isFormulaError(refs[i]))
265 | return refs[i];
266 | collection.add(this.extractRefValue(refs[i]).val, refs[i]);
267 | }
268 |
269 | // console.log('applyUnion', unions);
270 | return collection;
271 | }
272 |
273 | /**
274 | * Apply multiple references, e.g. A1:B3:C8:A:1:.....
275 | * @param refs
276 | // * @return {{ref: {from: {col: number, row: number}, to: {col: number, row: number}}}}
277 | */
278 | applyRange(refs) {
279 | let res, maxRow = -1, maxCol = -1, minRow = MAX_ROW + 1, minCol = MAX_COLUMN + 1;
280 | refs.forEach(ref => {
281 | if (this.isFormulaError(ref))
282 | return ref;
283 | // row ref is saved as number, parse the number to row ref here
284 | if (typeof ref === 'number') {
285 | ref = this.parseRow(ref);
286 | }
287 | ref = ref.ref;
288 | // check whole row/col reference
289 | if (ref.row === undefined) {
290 | minRow = 1;
291 | maxRow = MAX_ROW
292 | }
293 | if (ref.col === undefined) {
294 | minCol = 1;
295 | maxCol = MAX_COLUMN;
296 | }
297 |
298 | if (ref.row > maxRow)
299 | maxRow = ref.row;
300 | if (ref.row < minRow)
301 | minRow = ref.row;
302 | if (ref.col > maxCol)
303 | maxCol = ref.col;
304 | if (ref.col < minCol)
305 | minCol = ref.col;
306 | });
307 | if (maxRow === minRow && maxCol === minCol) {
308 | res = {
309 | ref: {
310 | row: maxRow,
311 | col: maxCol
312 | }
313 | }
314 | } else {
315 | res = {
316 | ref: {
317 | from: {row: minRow, col: minCol},
318 | to: {row: maxRow, col: maxCol}
319 | }
320 | };
321 | }
322 | return res;
323 | }
324 |
325 | /**
326 | * Throw away the refs, and retrieve the value.
327 | * @return {{val: *, isArray: boolean}}
328 | */
329 | extractRefValue(obj) {
330 | let res = obj, isArray = false;
331 | if (Array.isArray(res))
332 | isArray = true;
333 | if (obj.ref) {
334 | // can be number or array
335 | return {val: this.context.retrieveRef(obj), isArray};
336 |
337 | }
338 | return {val: res, isArray};
339 | }
340 |
341 | /**
342 | *
343 | * @param array
344 | * @return {Array}
345 | */
346 | toArray(array) {
347 | // TODO: check if array is valid
348 | // console.log('toArray', array);
349 | return array;
350 | }
351 |
352 | /**
353 | * @param {string} number
354 | * @return {number}
355 | */
356 | toNumber(number) {
357 | return Number(number);
358 | }
359 |
360 | /**
361 | * @param {string} string
362 | * @return {string}
363 | */
364 | toString(string) {
365 | return string.substring(1, string.length - 1) .replace(/""/g, '"');
366 | }
367 |
368 | /**
369 | * @param {string} bool
370 | * @return {boolean}
371 | */
372 | toBoolean(bool) {
373 | return bool === 'TRUE';
374 | }
375 |
376 | /**
377 | * Parse an error.
378 | * @param {string} error
379 | * @return {string}
380 | */
381 | toError(error) {
382 | return new FormulaError(error.toUpperCase());
383 | }
384 |
385 | isFormulaError(obj) {
386 | return obj instanceof FormulaError;
387 | }
388 |
389 | static formatChevrotainError(error, inputText) {
390 | let line, column, msg = '';
391 | // e.g. SUM(1))
392 | if (error instanceof NotAllInputParsedException) {
393 | line = error.token.startLine;
394 | column = error.token.startColumn;
395 | } else {
396 | line = error.previousToken.startLine;
397 | column = error.previousToken.startColumn + 1;
398 | }
399 |
400 | msg += '\n' + inputText.split('\n')[line - 1] + '\n';
401 | msg += Array(column - 1).fill(' ').join('') + '^\n';
402 | msg += `Error at position ${line}:${column}\n` + error.message;
403 | error.errorLocation = {line, column};
404 | return FormulaError.ERROR(msg, error);
405 | }
406 |
407 | }
408 |
409 | module.exports = Utils;
410 |
--------------------------------------------------------------------------------
/grammar/hooks.js:
--------------------------------------------------------------------------------
1 | const TextFunctions = require('../formulas/functions/text');
2 | const MathFunctions = require('../formulas/functions/math');
3 | const TrigFunctions = require('../formulas/functions/trigonometry');
4 | const LogicalFunctions = require('../formulas/functions/logical');
5 | const EngFunctions = require('../formulas/functions/engineering');
6 | const ReferenceFunctions = require('../formulas/functions/reference');
7 | const InformationFunctions = require('../formulas/functions/information');
8 | const StatisticalFunctions = require('../formulas/functions/statistical');
9 | const DateFunctions = require('../formulas/functions/date');
10 | const WebFunctions = require('../formulas/functions/web');
11 | const FormulaError = require('../formulas/error');
12 | const {FormulaHelpers} = require('../formulas/helpers');
13 | const {Parser, allTokens} = require('./parsing');
14 | const lexer = require('./lexing');
15 | const Utils = require('./utils');
16 |
17 | /**
18 | * A Excel Formula Parser & Evaluator
19 | */
20 | class FormulaParser {
21 |
22 | /**
23 | * @param {{functions: {}, functionsNeedContext: {}, onVariable: function, onCell: function, onRange: function}} [config]
24 | * @param isTest - is in testing environment
25 | */
26 | constructor(config, isTest = false) {
27 | this.logs = [];
28 | this.isTest = isTest;
29 | this.utils = new Utils(this);
30 | config = Object.assign({
31 | functions: {},
32 | functionsNeedContext: {},
33 | onVariable: () => null,
34 | onCell: () => 0,
35 | onRange: () => [[0]],
36 | }, config);
37 |
38 | this.onVariable = config.onVariable;
39 | this.functions = Object.assign({}, DateFunctions, StatisticalFunctions, InformationFunctions, ReferenceFunctions,
40 | EngFunctions, LogicalFunctions, TextFunctions, MathFunctions, TrigFunctions, WebFunctions,
41 | config.functions, config.functionsNeedContext);
42 | this.onRange = config.onRange;
43 | this.onCell = config.onCell;
44 |
45 | // functions treat null as 0, other functions treats null as ""
46 | this.funsNullAs0 = Object.keys(MathFunctions)
47 | .concat(Object.keys(TrigFunctions))
48 | .concat(Object.keys(LogicalFunctions))
49 | .concat(Object.keys(EngFunctions))
50 | .concat(Object.keys(ReferenceFunctions))
51 | .concat(Object.keys(StatisticalFunctions))
52 | .concat(Object.keys(DateFunctions));
53 |
54 | // functions need context and don't need to retrieve references
55 | this.funsNeedContextAndNoDataRetrieve = ['ROW', 'ROWS', 'COLUMN', 'COLUMNS', 'SUMIF', 'INDEX', 'AVERAGEIF', 'IF'];
56 |
57 | // functions need parser context
58 | this.funsNeedContext = [...Object.keys(config.functionsNeedContext), ...this.funsNeedContextAndNoDataRetrieve,
59 | 'INDEX', 'OFFSET', 'INDIRECT', 'IF', 'CHOOSE', 'WEBSERVICE'];
60 |
61 | // functions preserve reference in arguments
62 | this.funsPreserveRef = Object.keys(InformationFunctions);
63 |
64 | this.parser = new Parser(this, this.utils);
65 | }
66 |
67 | /**
68 | * Get all lexing token names. Webpack needs this.
69 | * @return {Array.} - All token names that should not be minimized.
70 | */
71 | static get allTokens() {
72 | return allTokens;
73 | }
74 |
75 | /**
76 | * Get value from the cell reference
77 | * @param ref
78 | * @return {*}
79 | */
80 | getCell(ref) {
81 | // console.log('get cell', JSON.stringify(ref));
82 | if (ref.sheet == null)
83 | ref.sheet = this.position ? this.position.sheet : undefined;
84 | return this.onCell(ref);
85 | }
86 |
87 | /**
88 | * Get values from the range reference.
89 | * @param ref
90 | * @return {*}
91 | */
92 | getRange(ref) {
93 | // console.log('get range', JSON.stringify(ref));
94 | if (ref.sheet == null)
95 | ref.sheet = this.position ? this.position.sheet : undefined;
96 | return this.onRange(ref)
97 | }
98 |
99 | /**
100 | * TODO:
101 | * Get references or values from a user defined variable.
102 | * @param name
103 | * @return {*}
104 | */
105 | getVariable(name) {
106 | // console.log('get variable', name);
107 | const res = {ref: this.onVariable(name, this.position.sheet, this.position)};
108 | if (res.ref == null)
109 | return FormulaError.NAME;
110 | return res;
111 | }
112 |
113 | /**
114 | * Retrieve values from the given reference.
115 | * @param valueOrRef
116 | * @return {*}
117 | */
118 | retrieveRef(valueOrRef) {
119 | if (FormulaHelpers.isRangeRef(valueOrRef)) {
120 | return this.getRange(valueOrRef.ref);
121 | }
122 | if (FormulaHelpers.isCellRef(valueOrRef)) {
123 | return this.getCell(valueOrRef.ref)
124 | }
125 | return valueOrRef;
126 | }
127 |
128 | /**
129 | * Call an excel function.
130 | * @param name - Function name.
131 | * @param args - Arguments that pass to the function.
132 | * @return {*}
133 | */
134 | _callFunction(name, args) {
135 | if (name.indexOf('_xlfn.') === 0)
136 | name = name.slice(6);
137 | name = name.toUpperCase();
138 | // if one arg is null, it means 0 or "" depends on the function it calls
139 | const nullValue = this.funsNullAs0.includes(name) ? 0 : '';
140 |
141 | if (!this.funsNeedContextAndNoDataRetrieve.includes(name)) {
142 | // retrieve reference
143 | args = args.map(arg => {
144 | if (arg === null)
145 | return {value: nullValue, isArray: false, omitted: true};
146 | const res = this.utils.extractRefValue(arg);
147 |
148 | if (this.funsPreserveRef.includes(name)) {
149 | return {value: res.val, isArray: res.isArray, ref: arg.ref};
150 | }
151 | return {
152 | value: res.val,
153 | isArray: res.isArray,
154 | isRangeRef: !!FormulaHelpers.isRangeRef(arg),
155 | isCellRef: !!FormulaHelpers.isCellRef(arg)
156 | };
157 | });
158 | }
159 | // console.log('callFunction', name, args)
160 |
161 | if (this.functions[name]) {
162 | let res;
163 | try {
164 | if (!this.funsNeedContextAndNoDataRetrieve.includes(name) && !this.funsNeedContext.includes(name))
165 | res = (this.functions[name](...args));
166 | else
167 | res = (this.functions[name](this, ...args));
168 | } catch (e) {
169 | // allow functions throw FormulaError, this make functions easier to implement!
170 | if (e instanceof FormulaError) {
171 | return e;
172 | } else {
173 | throw e;
174 | }
175 | }
176 | if (res === undefined) {
177 | // console.log(`Function ${name} may be not implemented.`);
178 | if (this.isTest) {
179 | if (!this.logs.includes(name)) this.logs.push(name);
180 | return {value: 0, ref: {}};
181 | }
182 | throw FormulaError.NOT_IMPLEMENTED(name);
183 | }
184 | return res;
185 | } else {
186 | // console.log(`Function ${name} is not implemented`);
187 | if (this.isTest) {
188 | if (!this.logs.includes(name)) this.logs.push(name);
189 | return {value: 0, ref: {}};
190 | }
191 | throw FormulaError.NOT_IMPLEMENTED(name);
192 | }
193 | }
194 |
195 | async callFunctionAsync(name, args) {
196 | const awaitedArgs = [];
197 | for (const arg of args) {
198 | awaitedArgs.push(await arg);
199 | }
200 | const res = await this._callFunction(name, awaitedArgs);
201 | return FormulaHelpers.checkFunctionResult(res)
202 | }
203 |
204 | callFunction(name, args) {
205 | if (this.async) {
206 | return this.callFunctionAsync(name, args);
207 | } else {
208 | const res = this._callFunction(name, args);
209 | return FormulaHelpers.checkFunctionResult(res);
210 | }
211 | }
212 |
213 | /**
214 | * Return currently supported functions.
215 | * @return {this}
216 | */
217 | supportedFunctions() {
218 | const supported = [];
219 | const functions = Object.keys(this.functions);
220 | functions.forEach(fun => {
221 | try {
222 | const res = this.functions[fun](0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
223 | if (res === undefined) return;
224 | supported.push(fun);
225 | } catch (e) {
226 | if (e instanceof Error)
227 | supported.push(fun);
228 | }
229 | });
230 | return supported.sort();
231 | }
232 |
233 | /**
234 | * Check and return the appropriate formula result.
235 | * @param result
236 | * @param {boolean} [allowReturnArray] - If the formula can return an array
237 | * @return {*}
238 | */
239 | checkFormulaResult(result, allowReturnArray = false) {
240 | const type = typeof result;
241 | // number
242 | if (type === 'number') {
243 | if (isNaN(result)) {
244 | return FormulaError.VALUE;
245 | } else if (!isFinite(result)) {
246 | return FormulaError.NUM;
247 | }
248 | result += 0; // make -0 to 0
249 | } else if (type === 'object') {
250 | if (result instanceof FormulaError)
251 | return result;
252 | if (allowReturnArray) {
253 | if (result.ref) {
254 | result = this.retrieveRef(result);
255 | }
256 | // Disallow union, and other unknown data types.
257 | // e.g. `=(A1:C1, A2:E9)` -> #VALUE!
258 | if (typeof result === 'object' && !Array.isArray(result) && result != null) {
259 | return FormulaError.VALUE;
260 | }
261 |
262 | } else {
263 | if (result.ref && result.ref.row && !result.ref.from) {
264 | // single cell reference
265 | result = this.retrieveRef(result);
266 | } else if (result.ref && result.ref.from && result.ref.from.col === result.ref.to.col) {
267 | // single Column reference
268 | result = this.retrieveRef({
269 | ref: {
270 | row: result.ref.from.row, col: result.ref.from.col
271 | }
272 | });
273 | } else if (Array.isArray(result)) {
274 | result = result[0][0]
275 | } else {
276 | // array, range reference, union collections
277 | return FormulaError.VALUE;
278 | }
279 | }
280 | }
281 | return result;
282 | }
283 |
284 | /**
285 | * Parse an excel formula.
286 | * @param {string} inputText
287 | * @param {{row: number, col: number}} [position] - The position of the parsed formula
288 | * e.g. {row: 1, col: 1}
289 | * @param {boolean} [allowReturnArray] - If the formula can return an array. Useful when parsing array formulas,
290 | * or data validation formulas.
291 | * @returns {*}
292 | */
293 | parse(inputText, position, allowReturnArray = false) {
294 | if (inputText.length === 0) throw Error('Input must not be empty.');
295 | this.position = position;
296 | this.async = false;
297 | const lexResult = lexer.lex(inputText);
298 | this.parser.input = lexResult.tokens;
299 | let res;
300 | try {
301 | res = this.parser.formulaWithBinaryOp();
302 | res = this.checkFormulaResult(res, allowReturnArray);
303 | if (res instanceof FormulaError) {
304 | return res;
305 | }
306 | } catch (e) {
307 | throw FormulaError.ERROR(e.message, e);
308 | }
309 | if (this.parser.errors.length > 0) {
310 | const error = this.parser.errors[0];
311 | throw Utils.formatChevrotainError(error, inputText);
312 | }
313 | return res;
314 | }
315 |
316 | /**
317 | * Parse an excel formula asynchronously.
318 | * Use when providing custom async functions.
319 | * @param {string} inputText
320 | * @param {{row: number, col: number}} [position] - The position of the parsed formula
321 | * e.g. {row: 1, col: 1}
322 | * @param {boolean} [allowReturnArray] - If the formula can return an array. Useful when parsing array formulas,
323 | * or data validation formulas.
324 | * @returns {*}
325 | */
326 | async parseAsync(inputText, position, allowReturnArray = false) {
327 | if (inputText.length === 0) throw Error('Input must not be empty.');
328 | this.position = position;
329 | this.async = true;
330 | const lexResult = lexer.lex(inputText);
331 | this.parser.input = lexResult.tokens;
332 | let res;
333 | try {
334 | res = await this.parser.formulaWithBinaryOp();
335 | res = this.checkFormulaResult(res, allowReturnArray);
336 | if (res instanceof FormulaError) {
337 | return res;
338 | }
339 | } catch (e) {
340 | throw FormulaError.ERROR(e.message, e);
341 | }
342 | if (this.parser.errors.length > 0) {
343 | const error = this.parser.errors[0];
344 | throw Utils.formatChevrotainError(error, inputText);
345 | }
346 | return res;
347 | }
348 | }
349 |
350 | module.exports = {
351 | FormulaParser,
352 | FormulaHelpers,
353 | };
354 |
--------------------------------------------------------------------------------
/grammar/parsing.js:
--------------------------------------------------------------------------------
1 | const lexer = require('./lexing');
2 | const {EmbeddedActionsParser} = require("chevrotain");
3 | const tokenVocabulary = lexer.tokenVocabulary;
4 | const {
5 | String,
6 | SheetQuoted,
7 | ExcelRefFunction,
8 | ExcelConditionalRefFunction,
9 | Function,
10 | FormulaErrorT,
11 | RefError,
12 | Cell,
13 | Sheet,
14 | Name,
15 | Number,
16 | Boolean,
17 | Column,
18 |
19 | // At,
20 | Comma,
21 | Colon,
22 | Semicolon,
23 | OpenParen,
24 | CloseParen,
25 | // OpenSquareParen,
26 | // CloseSquareParen,
27 | // ExclamationMark,
28 | OpenCurlyParen,
29 | CloseCurlyParen,
30 | MulOp,
31 | PlusOp,
32 | DivOp,
33 | MinOp,
34 | ConcatOp,
35 | ExOp,
36 | PercentOp,
37 | NeqOp,
38 | GteOp,
39 | LteOp,
40 | GtOp,
41 | EqOp,
42 | LtOp
43 | } = lexer.tokenVocabulary;
44 |
45 | class Parsing extends EmbeddedActionsParser {
46 | /**
47 | *
48 | * @param {FormulaParser|DepParser} context
49 | * @param {Utils} utils
50 | */
51 | constructor(context, utils) {
52 | super(tokenVocabulary, {
53 | outputCst: false,
54 | maxLookahead: 1,
55 | skipValidations: true,
56 | // traceInitPerf: true,
57 | });
58 | this.utils = utils;
59 | this.binaryOperatorsPrecedence = [
60 | ['^'],
61 | ['*', '/'],
62 | ['+', '-'],
63 | ['&'],
64 | ['<', '>', '=', '<>', '<=', '>='],
65 | ];
66 | const $ = this;
67 |
68 | // Adopted from https://github.com/spreadsheetlab/XLParser/blob/master/src/XLParser/ExcelFormulaGrammar.cs
69 |
70 | $.RULE('formulaWithBinaryOp', () => {
71 | const infixes = [];
72 | const values = [$.SUBRULE($.formulaWithPercentOp)];
73 | $.MANY(() => {
74 | // Caching Arrays of Alternatives
75 | // https://sap.github.io/chevrotain/docs/guide/performance.html#caching-arrays-of-alternatives
76 | infixes.push($.OR($.c1 ||
77 | (
78 | $.c1 = [
79 | {ALT: () => $.CONSUME(GtOp).image},
80 | {ALT: () => $.CONSUME(EqOp).image},
81 | {ALT: () => $.CONSUME(LtOp).image},
82 | {ALT: () => $.CONSUME(NeqOp).image},
83 | {ALT: () => $.CONSUME(GteOp).image},
84 | {ALT: () => $.CONSUME(LteOp).image},
85 | {ALT: () => $.CONSUME(ConcatOp).image},
86 | {ALT: () => $.CONSUME(PlusOp).image},
87 | {ALT: () => $.CONSUME(MinOp).image},
88 | {ALT: () => $.CONSUME(MulOp).image},
89 | {ALT: () => $.CONSUME(DivOp).image},
90 | {ALT: () => $.CONSUME(ExOp).image}
91 | ]
92 | )));
93 | values.push($.SUBRULE2($.formulaWithPercentOp));
94 | });
95 | $.ACTION(() => {
96 | // evaluate
97 | for (const ops of this.binaryOperatorsPrecedence) {
98 | for (let index = 0, length = infixes.length; index < length; index++) {
99 | const infix = infixes[index];
100 | if (!ops.includes(infix)) continue;
101 | infixes.splice(index, 1);
102 | values.splice(index, 2, this.utils.applyInfix(values[index], infix, values[index + 1]));
103 | index--;
104 | length--;
105 | }
106 | }
107 | });
108 |
109 | return values[0];
110 | });
111 |
112 | $.RULE('plusMinusOp', () => $.OR([
113 | {ALT: () => $.CONSUME(PlusOp).image},
114 | {ALT: () => $.CONSUME(MinOp).image}
115 | ]));
116 |
117 | $.RULE('formulaWithPercentOp', () => {
118 | let value = $.SUBRULE($.formulaWithUnaryOp);
119 | $.OPTION(() => {
120 | const postfix = $.CONSUME(PercentOp).image;
121 | value = $.ACTION(() => this.utils.applyPostfix(value, postfix));
122 | });
123 | return value;
124 | });
125 |
126 | $.RULE('formulaWithUnaryOp', () => {
127 | // support ++---3 => -3
128 | const prefixes = [];
129 | $.MANY(() => {
130 | const op = $.OR([
131 | {ALT: () => $.CONSUME(PlusOp).image},
132 | {ALT: () => $.CONSUME(MinOp).image}
133 | ]);
134 | prefixes.push(op);
135 | });
136 | const formula = $.SUBRULE($.formulaWithIntersect);
137 | if (prefixes.length > 0) return $.ACTION(() => this.utils.applyPrefix(prefixes, formula));
138 | return formula;
139 | });
140 |
141 |
142 | $.RULE('formulaWithIntersect', () => {
143 | // e.g. 'A1 A2 A3'
144 | let ref1 = $.SUBRULE($.formulaWithRange);
145 | const refs = [ref1];
146 | // console.log('check intersect')
147 | $.MANY({
148 | GATE: () => {
149 | // see https://github.com/SAP/chevrotain/blob/master/examples/grammars/css/css.js#L436-L441
150 | const prevToken = $.LA(0);
151 | const nextToken = $.LA(1);
152 | // This is the only place where the grammar is whitespace sensitive.
153 | return nextToken.startOffset > prevToken.endOffset + 1;
154 | },
155 | DEF: () => {
156 | refs.push($.SUBRULE3($.formulaWithRange));
157 | }
158 | });
159 | if (refs.length > 1) {
160 | return $.ACTION(() => $.ACTION(() => this.utils.applyIntersect(refs)))
161 | }
162 | return ref1;
163 | });
164 |
165 | $.RULE('formulaWithRange', () => {
166 | // e.g. 'A1:C3' or 'A1:A3:C4', can be any number of references, at lease 2
167 | const ref1 = $.SUBRULE($.formula);
168 | const refs = [ref1];
169 | $.MANY(() => {
170 | $.CONSUME(Colon);
171 | refs.push($.SUBRULE2($.formula));
172 | });
173 | if (refs.length > 1)
174 | return $.ACTION(() => $.ACTION(() => this.utils.applyRange(refs)));
175 | return ref1;
176 | });
177 |
178 | $.RULE('formula', () => $.OR9([
179 | {ALT: () => $.SUBRULE($.referenceWithoutInfix)},
180 | {ALT: () => $.SUBRULE($.paren)},
181 | {ALT: () => $.SUBRULE($.constant)},
182 | {ALT: () => $.SUBRULE($.functionCall)},
183 | {ALT: () => $.SUBRULE($.constantArray)},
184 | ]));
185 |
186 | $.RULE('paren', () => {
187 | // formula paren or union paren
188 | $.CONSUME(OpenParen);
189 | let result;
190 | const refs = [];
191 | refs.push($.SUBRULE($.formulaWithBinaryOp));
192 | $.MANY(() => {
193 | $.CONSUME(Comma);
194 | refs.push($.SUBRULE2($.formulaWithBinaryOp));
195 | });
196 | if (refs.length > 1)
197 | result = $.ACTION(() => this.utils.applyUnion(refs));
198 | else
199 | result = refs[0];
200 |
201 | $.CONSUME(CloseParen);
202 | return result;
203 | });
204 |
205 | $.RULE('constantArray', () => {
206 | // console.log('constantArray');
207 | const arr = [[]];
208 | let currentRow = 0;
209 | $.CONSUME(OpenCurlyParen);
210 |
211 | // array must contain at least one item
212 | arr[currentRow].push($.SUBRULE($.constantForArray));
213 | $.MANY(() => {
214 | const sep = $.OR([
215 | {ALT: () => $.CONSUME(Comma).image},
216 | {ALT: () => $.CONSUME(Semicolon).image}
217 | ]);
218 | const constant = $.SUBRULE2($.constantForArray);
219 | if (sep === ',') {
220 | arr[currentRow].push(constant)
221 | } else {
222 | currentRow++;
223 | arr[currentRow] = [];
224 | arr[currentRow].push(constant)
225 | }
226 | });
227 |
228 | $.CONSUME(CloseCurlyParen);
229 |
230 | return $.ACTION(() => this.utils.toArray(arr));
231 | });
232 |
233 | /**
234 | * Used in array
235 | */
236 | $.RULE('constantForArray', () => $.OR([
237 | {
238 | ALT: () => {
239 | const prefix = $.OPTION(() => $.SUBRULE($.plusMinusOp));
240 | const image = $.CONSUME(Number).image;
241 | const number = $.ACTION(() => this.utils.toNumber(image));
242 | if (prefix)
243 | return $.ACTION(() => this.utils.applyPrefix([prefix], number));
244 | return number;
245 | }
246 | }, {
247 | ALT: () => {
248 | const str = $.CONSUME(String).image;
249 | return $.ACTION(() => this.utils.toString(str));
250 | }
251 | }, {
252 | ALT: () => {
253 | const bool = $.CONSUME(Boolean).image;
254 | return $.ACTION(() => this.utils.toBoolean(bool));
255 | }
256 | }, {
257 | ALT: () => {
258 | const err = $.CONSUME(FormulaErrorT).image;
259 | return $.ACTION(() => this.utils.toError(err));
260 | }
261 | }, {
262 | ALT: () => {
263 | const err = $.CONSUME(RefError).image;
264 | return $.ACTION(() => this.utils.toError(err));
265 | }
266 | },
267 | ]));
268 |
269 | $.RULE('constant', () => $.OR([
270 | {
271 | ALT: () => {
272 | const number = $.CONSUME(Number).image;
273 | return $.ACTION(() => this.utils.toNumber(number));
274 | }
275 | }, {
276 | ALT: () => {
277 | const str = $.CONSUME(String).image;
278 | return $.ACTION(() => this.utils.toString(str));
279 | }
280 | }, {
281 | ALT: () => {
282 | const bool = $.CONSUME(Boolean).image;
283 | return $.ACTION(() => this.utils.toBoolean(bool));
284 | }
285 | }, {
286 | ALT: () => {
287 | const err = $.CONSUME(FormulaErrorT).image;
288 | return $.ACTION(() => this.utils.toError(err));
289 | }
290 | },
291 | ]));
292 |
293 | $.RULE('functionCall', () => {
294 | const functionName = $.CONSUME(Function).image.slice(0, -1);
295 | // console.log('functionName', functionName);
296 | const args = $.SUBRULE($.arguments);
297 | $.CONSUME(CloseParen);
298 | // dependency parser won't call function.
299 | return $.ACTION(() => context.callFunction(functionName, args));
300 |
301 | });
302 |
303 | $.RULE('arguments', () => {
304 | // console.log('try arguments')
305 |
306 | // allows ',' in the front
307 | $.MANY2(() => {
308 | $.CONSUME2(Comma);
309 | });
310 | const args = [];
311 | // allows empty arguments
312 | $.OPTION(() => {
313 | args.push($.SUBRULE($.formulaWithBinaryOp));
314 | $.MANY(() => {
315 | $.CONSUME1(Comma);
316 | args.push(null); // e.g. ROUND(1.5,)
317 | $.OPTION3(() => {
318 | args.pop();
319 | args.push($.SUBRULE2($.formulaWithBinaryOp))
320 | });
321 | });
322 | });
323 | return args;
324 | });
325 |
326 | $.RULE('referenceWithoutInfix', () => $.OR([
327 |
328 | {ALT: () => $.SUBRULE($.referenceItem)},
329 |
330 | {
331 | // sheet name prefix
332 | ALT: () => {
333 | // console.log('try sheetName');
334 | const sheetName = $.SUBRULE($.prefixName);
335 | // console.log('sheetName', sheetName);
336 | const referenceItem = $.SUBRULE2($.formulaWithRange);
337 |
338 | $.ACTION(() => {
339 | if (this.utils.isFormulaError(referenceItem))
340 | return referenceItem;
341 | referenceItem.ref.sheet = sheetName
342 | });
343 | return referenceItem;
344 | }
345 | },
346 |
347 | // {ALT: () => $.SUBRULE('dynamicDataExchange')},
348 | ]));
349 |
350 | $.RULE('referenceItem', () => $.OR([
351 | {
352 | ALT: () => {
353 | const address = $.CONSUME(Cell).image;
354 | return $.ACTION(() => this.utils.parseCellAddress(address));
355 | }
356 | },
357 | {
358 | ALT: () => {
359 | const name = $.CONSUME(Name).image;
360 | return $.ACTION(() => context.getVariable(name))
361 | }
362 | },
363 | {
364 | ALT: () => {
365 | const column = $.CONSUME(Column).image;
366 | return $.ACTION(() => this.utils.parseCol(column))
367 | }
368 | },
369 | // A row check should be here, but the token is same with Number,
370 | // In other to resolve ambiguities, I leave this empty, and
371 | // parse the number to row number when needed.
372 | {
373 | ALT: () => {
374 | const err = $.CONSUME(RefError).image;
375 | return $.ACTION(() => this.utils.toError(err))
376 | }
377 | },
378 | // {ALT: () => $.SUBRULE($.udfFunctionCall)},
379 | // {ALT: () => $.SUBRULE($.structuredReference)},
380 | ]));
381 |
382 | $.RULE('prefixName', () => $.OR([
383 | {ALT: () => $.CONSUME(Sheet).image.slice(0, -1)},
384 | {ALT: () => $.CONSUME(SheetQuoted).image.slice(1, -2).replace(/''/g, "'")},
385 | ]));
386 |
387 | this.performSelfAnalysis();
388 | }
389 | }
390 |
391 | module.exports = {
392 | Parser: Parsing,
393 | };
394 |
--------------------------------------------------------------------------------
/formulas/functions/reference.js:
--------------------------------------------------------------------------------
1 | const FormulaError = require('../error');
2 | const {FormulaHelpers, Types, WildCard, Address} = require('../helpers');
3 | const Collection = require('../../grammar/type/collection');
4 | const H = FormulaHelpers;
5 |
6 | const ReferenceFunctions = {
7 |
8 | ADDRESS: (rowNumber, columnNumber, absNum, a1, sheetText) => {
9 | rowNumber = H.accept(rowNumber, Types.NUMBER);
10 | columnNumber = H.accept(columnNumber, Types.NUMBER);
11 | absNum = H.accept(absNum, Types.NUMBER, 1);
12 | a1 = H.accept(a1, Types.BOOLEAN, true);
13 | sheetText = H.accept(sheetText, Types.STRING, '');
14 |
15 | if (rowNumber < 1 || columnNumber < 1 || absNum < 1 || absNum > 4)
16 | throw FormulaError.VALUE;
17 |
18 | let result = '';
19 | if (sheetText.length > 0) {
20 | if (/[^A-Za-z_.\d\u007F-\uFFFF]/.test(sheetText)) {
21 | result += `'${sheetText}'!`;
22 | } else {
23 | result += sheetText + '!';
24 | }
25 | }
26 | if (a1) {
27 | // A1 style
28 | result += (absNum === 1 || absNum === 3) ? '$' : '';
29 | result += Address.columnNumberToName(columnNumber);
30 | result += (absNum === 1 || absNum === 2) ? '$' : '';
31 | result += rowNumber;
32 | } else {
33 | // R1C1 style
34 | result += 'R';
35 | result += (absNum === 4 || absNum === 3) ? `[${rowNumber}]` : rowNumber;
36 | result += 'C';
37 | result += (absNum === 4 || absNum === 2) ? `[${columnNumber}]` : columnNumber;
38 | }
39 | return result;
40 | },
41 |
42 | AREAS: refs => {
43 | refs = H.accept(refs);
44 | if (refs instanceof Collection) {
45 | return refs.length;
46 | }
47 | return 1;
48 | },
49 |
50 | CHOOSE: (indexNum, ...values) => {
51 |
52 | },
53 |
54 | // Special
55 | COLUMN: (context, obj) => {
56 | if (obj == null) {
57 | if (context.position.col != null)
58 | return context.position.col;
59 | else
60 | throw Error('FormulaParser.parse is called without position parameter.')
61 | } else {
62 | if (typeof obj !== 'object' || Array.isArray(obj))
63 | throw FormulaError.VALUE;
64 | if (H.isCellRef(obj)) {
65 | return obj.ref.col;
66 | } else if (H.isRangeRef(obj)) {
67 | return obj.ref.from.col;
68 | } else {
69 | throw Error('ReferenceFunctions.COLUMN should not reach here.')
70 | }
71 | }
72 | },
73 |
74 | // Special
75 | COLUMNS: (context, obj) => {
76 | if (obj == null) {
77 | throw Error('COLUMNS requires one argument');
78 | }
79 | if (typeof obj != 'object' || Array.isArray(obj))
80 | throw FormulaError.VALUE;
81 | if (H.isCellRef(obj)) {
82 | return 1;
83 | } else if (H.isRangeRef(obj)) {
84 | return Math.abs(obj.ref.from.col - obj.ref.to.col) + 1;
85 | } else {
86 | throw Error('ReferenceFunctions.COLUMNS should not reach here.')
87 | }
88 | },
89 |
90 | HLOOKUP: (lookupValue, tableArray, rowIndexNum, rangeLookup) => {
91 | // preserve type of lookupValue
92 | lookupValue = H.accept(lookupValue);
93 | try {
94 | tableArray = H.accept(tableArray, Types.ARRAY, undefined, false);
95 | } catch (e) {
96 | // catch #VALUE! and throw #N/A
97 | if (e instanceof FormulaError)
98 | throw FormulaError.NA;
99 | throw e;
100 | }
101 | rowIndexNum = H.accept(rowIndexNum, Types.NUMBER);
102 | rangeLookup = H.accept(rangeLookup, Types.BOOLEAN, true);
103 |
104 | // check if rowIndexNum out of bound
105 | if (rowIndexNum < 1)
106 | throw FormulaError.VALUE;
107 | if (tableArray[rowIndexNum - 1] === undefined)
108 | throw FormulaError.REF;
109 |
110 | const lookupType = typeof lookupValue; // 'number', 'string', 'boolean'
111 |
112 | // approximate lookup (assume the array is sorted)
113 | if (rangeLookup) {
114 | let prevValue = lookupType === typeof tableArray[0][0] ? tableArray[0][0] : null;
115 | for (let i = 1; i < tableArray[0].length; i++) {
116 | const currValue = tableArray[0][i];
117 | const type = typeof currValue;
118 | // skip the value if type does not match
119 | if (type !== lookupType)
120 | continue;
121 | // if the previous two values are greater than lookup value, throw #N/A
122 | if (prevValue > lookupValue && currValue > lookupValue) {
123 | throw FormulaError.NA;
124 | }
125 | if (currValue === lookupValue)
126 | return tableArray[rowIndexNum - 1][i];
127 | // if previous value <= lookup value and current value > lookup value
128 | if (prevValue != null && currValue > lookupValue && prevValue <= lookupValue) {
129 | return tableArray[rowIndexNum - 1][i - 1];
130 | }
131 | prevValue = currValue;
132 | }
133 | if (prevValue == null)
134 | throw FormulaError.NA;
135 | if (tableArray[0].length === 1) {
136 | return tableArray[rowIndexNum - 1][0]
137 | }
138 | return prevValue;
139 | }
140 | // exact lookup with wildcard support
141 | else {
142 | let index = -1;
143 | if (WildCard.isWildCard(lookupValue)) {
144 | index = tableArray[0].findIndex(item => {
145 | return WildCard.toRegex(lookupValue, 'i').test(item);
146 | });
147 | } else {
148 | index = tableArray[0].findIndex(item => {
149 | return item === lookupValue;
150 | });
151 | }
152 | // the exact match is not found
153 | if (index === -1) throw FormulaError.NA;
154 | return tableArray[rowIndexNum - 1][index];
155 | }
156 | },
157 |
158 | // Special
159 | INDEX: (context, ranges, rowNum, colNum, areaNum) => {
160 | // retrieve values
161 | rowNum = context.utils.extractRefValue(rowNum);
162 | rowNum = {value: rowNum.val, isArray: rowNum.isArray};
163 | rowNum = H.accept(rowNum, Types.NUMBER);
164 | rowNum = Math.trunc(rowNum);
165 |
166 | if (colNum == null) {
167 | colNum = 1;
168 | } else {
169 | colNum = context.utils.extractRefValue(colNum);
170 | colNum = {value: colNum.val, isArray: colNum.isArray};
171 | colNum = H.accept(colNum, Types.NUMBER, 1);
172 | colNum = Math.trunc(colNum);
173 | }
174 |
175 | if (areaNum == null) {
176 | areaNum = 1;
177 | } else {
178 | areaNum = context.utils.extractRefValue(areaNum);
179 | areaNum = {value: areaNum.val, isArray: areaNum.isArray};
180 | areaNum = H.accept(areaNum, Types.NUMBER, 1);
181 | areaNum = Math.trunc(areaNum);
182 | }
183 |
184 | // get the range area that we want to index
185 | // ranges can be cell ref, range ref or array constant
186 | let range = ranges;
187 | // many ranges (Reference form)
188 | if (ranges instanceof Collection) {
189 | range = ranges.refs[areaNum - 1];
190 | } else if (areaNum > 1) {
191 | throw FormulaError.REF;
192 | }
193 |
194 | if (rowNum === 0 && colNum === 0) {
195 | return range;
196 | }
197 |
198 | // query the whole column
199 | if (rowNum === 0) {
200 | if (H.isRangeRef(range)) {
201 | if (range.ref.to.col - range.ref.from.col < colNum - 1)
202 | throw FormulaError.REF;
203 | range.ref.from.col += colNum - 1;
204 | range.ref.to.col = range.ref.from.col;
205 | return range;
206 | } else if (Array.isArray(range)) {
207 | const res = [];
208 | range.forEach(row => res.push([row[colNum - 1]]));
209 | return res;
210 | }
211 | }
212 | // query the whole row
213 | if (colNum === 0) {
214 | if (H.isRangeRef(range)) {
215 | if (range.ref.to.row - range.ref.from.row < rowNum - 1)
216 | throw FormulaError.REF;
217 | range.ref.from.row += rowNum - 1;
218 | range.ref.to.row = range.ref.from.row;
219 | return range;
220 | } else if (Array.isArray(range)) {
221 | return range[colNum - 1];
222 | }
223 | }
224 | // query single cell
225 | if (rowNum !== 0 && colNum !== 0) {
226 | // range reference
227 | if (H.isRangeRef(range)) {
228 | range = range.ref;
229 | if (range.to.row - range.from.row < rowNum - 1 || range.to.col - range.from.col < colNum - 1)
230 | throw FormulaError.REF;
231 | return {ref: {row: range.from.row + rowNum - 1, col: range.from.col + colNum - 1}};
232 | }
233 | // cell reference
234 | else if (H.isCellRef(range)) {
235 | range = range.ref;
236 | if (rowNum > 1 || colNum > 1)
237 | throw FormulaError.REF;
238 | return {ref: {row: range.row + rowNum - 1, col: range.col + colNum - 1}};
239 | }
240 | // array constant
241 | else if (Array.isArray(range)) {
242 | if (range.length < rowNum || range[0].length < colNum)
243 | throw FormulaError.REF;
244 | return range[rowNum - 1][colNum - 1];
245 | }
246 | }
247 | },
248 |
249 | MATCH: () => {
250 |
251 | },
252 |
253 | // Special
254 | ROW: (context, obj) => {
255 | if (obj == null) {
256 | if (context.position.row != null)
257 | return context.position.row;
258 | else
259 | throw Error('FormulaParser.parse is called without position parameter.')
260 | } else {
261 | if (typeof obj !== 'object' || Array.isArray(obj))
262 | throw FormulaError.VALUE;
263 | if (H.isCellRef(obj)) {
264 | return obj.ref.row;
265 | } else if (H.isRangeRef(obj)) {
266 | return obj.ref.from.row;
267 | } else {
268 | throw Error('ReferenceFunctions.ROW should not reach here.')
269 | }
270 | }
271 | },
272 |
273 | // Special
274 | ROWS: (context, obj) => {
275 | if (obj == null) {
276 | throw Error('ROWS requires one argument');
277 | }
278 | if (typeof obj != 'object' || Array.isArray(obj))
279 | throw FormulaError.VALUE;
280 | if (H.isCellRef(obj)) {
281 | return 1;
282 | } else if (H.isRangeRef(obj)) {
283 | return Math.abs(obj.ref.from.row - obj.ref.to.row) + 1;
284 | } else {
285 | throw Error('ReferenceFunctions.ROWS should not reach here.')
286 | }
287 | },
288 |
289 | TRANSPOSE: (array) => {
290 | array = H.accept(array, Types.ARRAY, undefined, false);
291 | // https://github.com/numbers/numbers.js/blob/master/lib/numbers/matrix.js#L171
292 | const result = [];
293 |
294 | for (let i = 0; i < array[0].length; i++) {
295 | result[i] = [];
296 |
297 | for (let j = 0; j < array.length; j++) {
298 | result[i][j] = array[j][i];
299 | }
300 | }
301 |
302 | return result;
303 | },
304 |
305 | VLOOKUP: (lookupValue, tableArray, colIndexNum, rangeLookup) => {
306 | // preserve type of lookupValue
307 | lookupValue = H.accept(lookupValue);
308 | try {
309 | tableArray = H.accept(tableArray, Types.ARRAY, undefined, false);
310 | } catch (e) {
311 | // catch #VALUE! and throw #N/A
312 | if (e instanceof FormulaError)
313 | throw FormulaError.NA;
314 | throw e;
315 | }
316 | colIndexNum = H.accept(colIndexNum, Types.NUMBER);
317 | rangeLookup = H.accept(rangeLookup, Types.BOOLEAN, true);
318 |
319 | // check if colIndexNum out of bound
320 | if (colIndexNum < 1)
321 | throw FormulaError.VALUE;
322 | if (tableArray[0][colIndexNum - 1] === undefined)
323 | throw FormulaError.REF;
324 |
325 | const lookupType = typeof lookupValue; // 'number', 'string', 'boolean'
326 |
327 | // approximate lookup (assume the array is sorted)
328 | if (rangeLookup) {
329 | let prevValue = lookupType === typeof tableArray[0][0] ? tableArray[0][0] : null;
330 | for (let i = 1; i < tableArray.length; i++) {
331 | const currRow = tableArray[i];
332 | const currValue = tableArray[i][0];
333 | const type = typeof currValue;
334 | // skip the value if type does not match
335 | if (type !== lookupType)
336 | continue;
337 | // if the previous two values are greater than lookup value, throw #N/A
338 | if (prevValue > lookupValue && currValue > lookupValue) {
339 | throw FormulaError.NA;
340 | }
341 | if (currValue === lookupValue)
342 | return currRow[colIndexNum - 1];
343 | // if previous value <= lookup value and current value > lookup value
344 | if (prevValue != null && currValue > lookupValue && prevValue <= lookupValue) {
345 | return tableArray[i - 1][colIndexNum - 1];
346 | }
347 | prevValue = currValue;
348 | }
349 | if (prevValue == null)
350 | throw FormulaError.NA;
351 | if (tableArray.length === 1) {
352 | return tableArray[0][colIndexNum - 1]
353 | }
354 | return prevValue;
355 | }
356 | // exact lookup with wildcard support
357 | else {
358 | let index = -1;
359 | if (WildCard.isWildCard(lookupValue)) {
360 | index = tableArray.findIndex(currRow => {
361 | return WildCard.toRegex(lookupValue, 'i').test(currRow[0]);
362 | });
363 | } else {
364 | index = tableArray.findIndex(currRow => {
365 | return currRow[0] === lookupValue;
366 | });
367 | }
368 | // the exact match is not found
369 | if (index === -1) throw FormulaError.NA;
370 | return tableArray[index][colIndexNum - 1];
371 | }
372 | },
373 | };
374 |
375 | module.exports = ReferenceFunctions;
376 |
--------------------------------------------------------------------------------