├── .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 | '1&#REF!': 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 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: // 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 | --------------------------------------------------------------------------------