├── .gitignore ├── src ├── contains.js ├── token.js ├── substitute.js ├── get-symbols.js ├── instruction.js ├── expression.js ├── simplify.js ├── expression-to-string.js ├── parser.js ├── evaluate.js ├── functions.js ├── parser-state.js └── token-stream.js ├── rollup-esm.config.js ├── rollup.config.js ├── test ├── lib │ └── spy.js ├── functions.js ├── parser.js ├── expression.js └── operators.js ├── .travis.yml ├── rollup-min.config.js ├── .eslintrc.json ├── bower.json ├── index.js ├── LICENSE.txt ├── CHANGELOG.md ├── package.json ├── parser.d.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .idea 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /src/contains.js: -------------------------------------------------------------------------------- 1 | export default function contains(array, obj) { 2 | for (var i = 0; i < array.length; i++) { 3 | if (array[i] === obj) { 4 | return true; 5 | } 6 | } 7 | return false; 8 | } 9 | -------------------------------------------------------------------------------- /rollup-esm.config.js: -------------------------------------------------------------------------------- 1 | import rollupConfig from './rollup.config'; 2 | 3 | rollupConfig.plugins = []; 4 | rollupConfig.output.file = 'dist/index.mjs'; 5 | rollupConfig.output.format = 'esm'; 6 | 7 | export default rollupConfig; 8 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'index.js', 3 | output: { 4 | file: 'dist/bundle.js', 5 | format: 'umd', 6 | name: 'exprEval', 7 | exports: 'named' 8 | }, 9 | plugins: [] 10 | }; 11 | -------------------------------------------------------------------------------- /test/lib/spy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (fn) { 4 | function spy() { 5 | spy.called = true; 6 | return fn.apply(this, arguments); 7 | } 8 | spy.called = false; 9 | return spy; 10 | }; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "11" 5 | - "10" 6 | - "9" 7 | - "8" 8 | - "7" 9 | - "6" 10 | 11 | install: 12 | - npm install 13 | - npm run build 14 | 15 | script: 16 | - mocha 17 | -------------------------------------------------------------------------------- /rollup-min.config.js: -------------------------------------------------------------------------------- 1 | import rollupConfig from './rollup.config'; 2 | import { uglify } from 'rollup-plugin-uglify'; 3 | 4 | rollupConfig.plugins = [ uglify() ]; 5 | rollupConfig.output.file = 'dist/bundle.min.js'; 6 | 7 | export default rollupConfig; 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "semistandard", 3 | "rules": { 4 | "space-before-function-paren": [ 5 | "error", { 6 | "anonymous": "always", 7 | "named": "never" 8 | } 9 | ], 10 | "linebreak-style": [ 11 | "error", 12 | "unix" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/token.js: -------------------------------------------------------------------------------- 1 | export var TEOF = 'TEOF'; 2 | export var TOP = 'TOP'; 3 | export var TNUMBER = 'TNUMBER'; 4 | export var TSTRING = 'TSTRING'; 5 | export var TPAREN = 'TPAREN'; 6 | export var TBRACKET = 'TBRACKET'; 7 | export var TCOMMA = 'TCOMMA'; 8 | export var TNAME = 'TNAME'; 9 | export var TSEMICOLON = 'TSEMICOLON'; 10 | 11 | export function Token(type, value, index) { 12 | this.type = type; 13 | this.value = value; 14 | this.index = index; 15 | } 16 | 17 | Token.prototype.toString = function () { 18 | return this.type + ': ' + this.value; 19 | }; 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expr-eval", 3 | "description": "Mathematical expression evaluator", 4 | "main": "dist/bundle.js", 5 | "authors": [ 6 | "Matthew Crumley " 7 | ], 8 | "license": "MIT", 9 | "keywords": [ 10 | "expression", 11 | "math", 12 | "evaluate", 13 | "eval", 14 | "function", 15 | "parser" 16 | ], 17 | "homepage": "https://github.com/silentmatt/expr-eval", 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/silentmatt/expr-eval.git" 21 | }, 22 | "ignore": [ 23 | "*", 24 | "!dist/", 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Based on ndef.parser, by Raphael Graf(r@undefined.ch) 3 | http://www.undefined.ch/mparser/index.html 4 | 5 | Ported to JavaScript and modified by Matthew Crumley (email@matthewcrumley.com, http://silentmatt.com/) 6 | 7 | You are free to use and modify this code in anyway you find useful. Please leave this comment in the code 8 | to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, 9 | but don't feel like you have to let me know or ask permission. 10 | */ 11 | 12 | import { Expression } from './src/expression'; 13 | import { Parser } from './src/parser'; 14 | 15 | export { 16 | Expression, 17 | Parser 18 | }; 19 | 20 | // Backwards compatibility 21 | export default{ 22 | Parser: Parser, 23 | Expression: Expression 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matthew Crumley 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 | -------------------------------------------------------------------------------- /src/substitute.js: -------------------------------------------------------------------------------- 1 | import { Instruction, IOP1, IOP2, IOP3, IVAR, IEXPR, ternaryInstruction, binaryInstruction, unaryInstruction } from './instruction'; 2 | 3 | export default function substitute(tokens, variable, expr) { 4 | var newexpression = []; 5 | for (var i = 0; i < tokens.length; i++) { 6 | var item = tokens[i]; 7 | var type = item.type; 8 | if (type === IVAR && item.value === variable) { 9 | for (var j = 0; j < expr.tokens.length; j++) { 10 | var expritem = expr.tokens[j]; 11 | var replitem; 12 | if (expritem.type === IOP1) { 13 | replitem = unaryInstruction(expritem.value); 14 | } else if (expritem.type === IOP2) { 15 | replitem = binaryInstruction(expritem.value); 16 | } else if (expritem.type === IOP3) { 17 | replitem = ternaryInstruction(expritem.value); 18 | } else { 19 | replitem = new Instruction(expritem.type, expritem.value); 20 | } 21 | newexpression.push(replitem); 22 | } 23 | } else if (type === IEXPR) { 24 | newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr))); 25 | } else { 26 | newexpression.push(item); 27 | } 28 | } 29 | return newexpression; 30 | } 31 | -------------------------------------------------------------------------------- /src/get-symbols.js: -------------------------------------------------------------------------------- 1 | import { IVAR, IMEMBER, IEXPR, IVARNAME } from './instruction'; 2 | import contains from './contains'; 3 | 4 | export default function getSymbols(tokens, symbols, options) { 5 | options = options || {}; 6 | var withMembers = !!options.withMembers; 7 | var prevVar = null; 8 | 9 | for (var i = 0; i < tokens.length; i++) { 10 | var item = tokens[i]; 11 | if (item.type === IVAR || item.type === IVARNAME) { 12 | if (!withMembers && !contains(symbols, item.value)) { 13 | symbols.push(item.value); 14 | } else if (prevVar !== null) { 15 | if (!contains(symbols, prevVar)) { 16 | symbols.push(prevVar); 17 | } 18 | prevVar = item.value; 19 | } else { 20 | prevVar = item.value; 21 | } 22 | } else if (item.type === IMEMBER && withMembers && prevVar !== null) { 23 | prevVar += '.' + item.value; 24 | } else if (item.type === IEXPR) { 25 | getSymbols(item.value, symbols, options); 26 | } else if (prevVar !== null) { 27 | if (!contains(symbols, prevVar)) { 28 | symbols.push(prevVar); 29 | } 30 | prevVar = null; 31 | } 32 | } 33 | 34 | if (prevVar !== null && !contains(symbols, prevVar)) { 35 | symbols.push(prevVar); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/instruction.js: -------------------------------------------------------------------------------- 1 | export var INUMBER = 'INUMBER'; 2 | export var IOP1 = 'IOP1'; 3 | export var IOP2 = 'IOP2'; 4 | export var IOP3 = 'IOP3'; 5 | export var IVAR = 'IVAR'; 6 | export var IVARNAME = 'IVARNAME'; 7 | export var IFUNCALL = 'IFUNCALL'; 8 | export var IFUNDEF = 'IFUNDEF'; 9 | export var IEXPR = 'IEXPR'; 10 | export var IEXPREVAL = 'IEXPREVAL'; 11 | export var IMEMBER = 'IMEMBER'; 12 | export var IENDSTATEMENT = 'IENDSTATEMENT'; 13 | export var IARRAY = 'IARRAY'; 14 | 15 | export function Instruction(type, value) { 16 | this.type = type; 17 | this.value = (value !== undefined && value !== null) ? value : 0; 18 | } 19 | 20 | Instruction.prototype.toString = function () { 21 | switch (this.type) { 22 | case INUMBER: 23 | case IOP1: 24 | case IOP2: 25 | case IOP3: 26 | case IVAR: 27 | case IVARNAME: 28 | case IENDSTATEMENT: 29 | return this.value; 30 | case IFUNCALL: 31 | return 'CALL ' + this.value; 32 | case IFUNDEF: 33 | return 'DEF ' + this.value; 34 | case IARRAY: 35 | return 'ARRAY ' + this.value; 36 | case IMEMBER: 37 | return '.' + this.value; 38 | default: 39 | return 'Invalid Instruction'; 40 | } 41 | }; 42 | 43 | export function unaryInstruction(value) { 44 | return new Instruction(IOP1, value); 45 | } 46 | 47 | export function binaryInstruction(value) { 48 | return new Instruction(IOP2, value); 49 | } 50 | 51 | export function ternaryInstruction(value) { 52 | return new Instruction(IOP3, value); 53 | } 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.2] - 2019-09-28 4 | 5 | ### Added 6 | 7 | - Added non-default exports when using the ES module format. This allows `import { Parser } from 'expr-eval'` to work in TypeScript. The default export is still available for backward compatibility. 8 | 9 | 10 | ## [2.0.1] - 2019-09-10 11 | 12 | ### Added 13 | 14 | - Added the `if(condition, trueValue, falseValue)` function back. The ternary operator is still recommended if you need to only evaluate one branch, but we're keep this as an option at least for now. 15 | 16 | 17 | ## [2.0.0] - 2019-09-07 18 | 19 | ### Added 20 | 21 | - Better support for arrays, including literals: `[ 1, 2, 3 ]` and indexing: `array[0]` 22 | - New functions for arrays: `join`, `indexOf`, `map`, `filter`, and `fold` 23 | - Variable assignment: `x = 4` 24 | - Custom function definitions: `myfunction(x, y) = x * y` 25 | - Evaluate multiple expressions by separating them with `;` 26 | - New operators: `log2` (base-2 logarithm), `cbrt` (cube root), `expm1` (`e^x - 1`), `log1p` (`log(1 + x)`), `sign` (essentially `x == 0 ? 0 : x / abs x`) 27 | 28 | ### Changed 29 | 30 | - `min` and `max` functions accept either a parameter list or a single array argument 31 | - `in` operator is enabled by default. It can be disabled by passing { operators: `{ 'in': false } }` to the `Parser` constructor. 32 | - `||` (concatenation operator) now supports strings and arrays 33 | 34 | ### Removed 35 | 36 | - Removed the `if(condition, trueValue, falseValue)` function. Use the ternary conditional operator instead: `condition ? trueValue : falseValue`, or you can add it back easily with a custom function. 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expr-eval", 3 | "version": "2.0.2", 4 | "description": "Mathematical expression evaluator", 5 | "main": "dist/bundle.js", 6 | "module": "dist/index.mjs", 7 | "typings": "parser.d.ts", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "eslint": "^6.3.0", 14 | "eslint-config-semistandard": "^15.0.0", 15 | "eslint-config-standard": "^13.0.1", 16 | "eslint-plugin-import": "^2.15.0", 17 | "eslint-plugin-node": "^9.2.0", 18 | "eslint-plugin-promise": "^4.0.1", 19 | "eslint-plugin-standard": "^4.0.0", 20 | "mocha": "^6.2.0", 21 | "nyc": "^14.1.1", 22 | "rollup": "^1.20.3", 23 | "rollup-plugin-uglify": "^6.0.3" 24 | }, 25 | "scripts": { 26 | "test": "npm run build && mocha", 27 | "coverage": "npm run build && nyc --reporter=lcov --reporter=text-summary mocha", 28 | "lint": "eslint index.js src test rollup.config.js rollup-min.config.js", 29 | "watch": "rollup -c rollup.config.js -w", 30 | "build": "rollup -c rollup.config.js && rollup -c rollup-min.config.js && rollup -c rollup-esm.config.js", 31 | "prepublish": "npm run build" 32 | }, 33 | "files": [ 34 | "dist/", 35 | "parser.d.ts" 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/silentmatt/expr-eval.git" 40 | }, 41 | "keywords": [ 42 | "expression", 43 | "math", 44 | "evaluate", 45 | "eval", 46 | "function", 47 | "parser" 48 | ], 49 | "author": "Matthew Crumley", 50 | "license": "MIT", 51 | "bugs": { 52 | "url": "https://github.com/silentmatt/expr-eval/issues" 53 | }, 54 | "homepage": "https://github.com/silentmatt/expr-eval#readme" 55 | } 56 | -------------------------------------------------------------------------------- /src/expression.js: -------------------------------------------------------------------------------- 1 | import simplify from './simplify'; 2 | import substitute from './substitute'; 3 | import evaluate from './evaluate'; 4 | import expressionToString from './expression-to-string'; 5 | import getSymbols from './get-symbols'; 6 | 7 | export function Expression(tokens, parser) { 8 | this.tokens = tokens; 9 | this.parser = parser; 10 | this.unaryOps = parser.unaryOps; 11 | this.binaryOps = parser.binaryOps; 12 | this.ternaryOps = parser.ternaryOps; 13 | this.functions = parser.functions; 14 | } 15 | 16 | Expression.prototype.simplify = function (values) { 17 | values = values || {}; 18 | return new Expression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser); 19 | }; 20 | 21 | Expression.prototype.substitute = function (variable, expr) { 22 | if (!(expr instanceof Expression)) { 23 | expr = this.parser.parse(String(expr)); 24 | } 25 | 26 | return new Expression(substitute(this.tokens, variable, expr), this.parser); 27 | }; 28 | 29 | Expression.prototype.evaluate = function (values) { 30 | values = values || {}; 31 | return evaluate(this.tokens, this, values); 32 | }; 33 | 34 | Expression.prototype.toString = function () { 35 | return expressionToString(this.tokens, false); 36 | }; 37 | 38 | Expression.prototype.symbols = function (options) { 39 | options = options || {}; 40 | var vars = []; 41 | getSymbols(this.tokens, vars, options); 42 | return vars; 43 | }; 44 | 45 | Expression.prototype.variables = function (options) { 46 | options = options || {}; 47 | var vars = []; 48 | getSymbols(this.tokens, vars, options); 49 | var functions = this.functions; 50 | return vars.filter(function (name) { 51 | return !(name in functions); 52 | }); 53 | }; 54 | 55 | Expression.prototype.toJSFunction = function (param, variables) { 56 | var expr = this; 57 | var f = new Function(param, 'with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return ' + expressionToString(this.simplify(variables).tokens, true) + '; }'); // eslint-disable-line no-new-func 58 | return function () { 59 | return f.apply(expr, arguments); 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /parser.d.ts: -------------------------------------------------------------------------------- 1 | export type Value = number 2 | | string 3 | | ((...args: Value[]) => Value) 4 | | { [propertyName: string]: Value }; 5 | 6 | export interface Values { 7 | [propertyName: string]: Value; 8 | } 9 | 10 | export interface ParserOptions { 11 | allowMemberAccess?: boolean; 12 | operators?: { 13 | add?: boolean, 14 | comparison?: boolean, 15 | concatenate?: boolean, 16 | conditional?: boolean, 17 | divide?: boolean, 18 | factorial?: boolean, 19 | logical?: boolean, 20 | multiply?: boolean, 21 | power?: boolean, 22 | remainder?: boolean, 23 | subtract?: boolean, 24 | sin?: boolean, 25 | cos?: boolean, 26 | tan?: boolean, 27 | asin?: boolean, 28 | acos?: boolean, 29 | atan?: boolean, 30 | sinh?: boolean, 31 | cosh?: boolean, 32 | tanh?: boolean, 33 | asinh?: boolean, 34 | acosh?: boolean, 35 | atanh?: boolean, 36 | sqrt?: boolean, 37 | log?: boolean, 38 | ln?: boolean, 39 | lg?: boolean, 40 | log10?: boolean, 41 | abs?: boolean, 42 | ceil?: boolean, 43 | floor?: boolean, 44 | round?: boolean, 45 | trunc?: boolean, 46 | exp?: boolean, 47 | length?: boolean, 48 | in?: boolean, 49 | random?: boolean, 50 | min?: boolean, 51 | max?: boolean, 52 | assignment?: boolean, 53 | fndef?: boolean, 54 | cbrt?: boolean, 55 | expm1?: boolean, 56 | log1p?: boolean, 57 | sign?: boolean, 58 | log2?: boolean 59 | }; 60 | } 61 | 62 | export class Parser { 63 | constructor(options?: ParserOptions); 64 | unaryOps: any; 65 | functions: any; 66 | consts: any; 67 | parse(expression: string): Expression; 68 | evaluate(expression: string, values?: Value): number; 69 | static parse(expression: string): Expression; 70 | static evaluate(expression: string, values?: Value): number; 71 | } 72 | 73 | export interface Expression { 74 | simplify(values?: Value): Expression; 75 | evaluate(values?: Value): any; 76 | substitute(variable: string, value: Expression | string | number): Expression; 77 | symbols(options?: { withMembers?: boolean }): string[]; 78 | variables(options?: { withMembers?: boolean }): string[]; 79 | toJSFunction(params: string | string[], values?: Value): (...args: any[]) => number; 80 | } 81 | -------------------------------------------------------------------------------- /src/simplify.js: -------------------------------------------------------------------------------- 1 | import { Instruction, INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IEXPR, IMEMBER, IARRAY } from './instruction'; 2 | 3 | export default function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) { 4 | var nstack = []; 5 | var newexpression = []; 6 | var n1, n2, n3; 7 | var f; 8 | for (var i = 0; i < tokens.length; i++) { 9 | var item = tokens[i]; 10 | var type = item.type; 11 | if (type === INUMBER || type === IVARNAME) { 12 | if (Array.isArray(item.value)) { 13 | nstack.push.apply(nstack, simplify(item.value.map(function (x) { 14 | return new Instruction(INUMBER, x); 15 | }).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values)); 16 | } else { 17 | nstack.push(item); 18 | } 19 | } else if (type === IVAR && values.hasOwnProperty(item.value)) { 20 | item = new Instruction(INUMBER, values[item.value]); 21 | nstack.push(item); 22 | } else if (type === IOP2 && nstack.length > 1) { 23 | n2 = nstack.pop(); 24 | n1 = nstack.pop(); 25 | f = binaryOps[item.value]; 26 | item = new Instruction(INUMBER, f(n1.value, n2.value)); 27 | nstack.push(item); 28 | } else if (type === IOP3 && nstack.length > 2) { 29 | n3 = nstack.pop(); 30 | n2 = nstack.pop(); 31 | n1 = nstack.pop(); 32 | if (item.value === '?') { 33 | nstack.push(n1.value ? n2.value : n3.value); 34 | } else { 35 | f = ternaryOps[item.value]; 36 | item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value)); 37 | nstack.push(item); 38 | } 39 | } else if (type === IOP1 && nstack.length > 0) { 40 | n1 = nstack.pop(); 41 | f = unaryOps[item.value]; 42 | item = new Instruction(INUMBER, f(n1.value)); 43 | nstack.push(item); 44 | } else if (type === IEXPR) { 45 | while (nstack.length > 0) { 46 | newexpression.push(nstack.shift()); 47 | } 48 | newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values))); 49 | } else if (type === IMEMBER && nstack.length > 0) { 50 | n1 = nstack.pop(); 51 | nstack.push(new Instruction(INUMBER, n1.value[item.value])); 52 | } /* else if (type === IARRAY && nstack.length >= item.value) { 53 | var length = item.value; 54 | while (length-- > 0) { 55 | newexpression.push(nstack.pop()); 56 | } 57 | newexpression.push(new Instruction(IARRAY, item.value)); 58 | } */ else { 59 | while (nstack.length > 0) { 60 | newexpression.push(nstack.shift()); 61 | } 62 | newexpression.push(item); 63 | } 64 | } 65 | while (nstack.length > 0) { 66 | newexpression.push(nstack.shift()); 67 | } 68 | return newexpression; 69 | } 70 | -------------------------------------------------------------------------------- /src/expression-to-string.js: -------------------------------------------------------------------------------- 1 | import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IMEMBER, IENDSTATEMENT, IARRAY } from './instruction'; 2 | 3 | export default function expressionToString(tokens, toJS) { 4 | var nstack = []; 5 | var n1, n2, n3; 6 | var f, args, argCount; 7 | for (var i = 0; i < tokens.length; i++) { 8 | var item = tokens[i]; 9 | var type = item.type; 10 | if (type === INUMBER) { 11 | if (typeof item.value === 'number' && item.value < 0) { 12 | nstack.push('(' + item.value + ')'); 13 | } else if (Array.isArray(item.value)) { 14 | nstack.push('[' + item.value.map(escapeValue).join(', ') + ']'); 15 | } else { 16 | nstack.push(escapeValue(item.value)); 17 | } 18 | } else if (type === IOP2) { 19 | n2 = nstack.pop(); 20 | n1 = nstack.pop(); 21 | f = item.value; 22 | if (toJS) { 23 | if (f === '^') { 24 | nstack.push('Math.pow(' + n1 + ', ' + n2 + ')'); 25 | } else if (f === 'and') { 26 | nstack.push('(!!' + n1 + ' && !!' + n2 + ')'); 27 | } else if (f === 'or') { 28 | nstack.push('(!!' + n1 + ' || !!' + n2 + ')'); 29 | } else if (f === '||') { 30 | nstack.push('(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((' + n1 + '),(' + n2 + ')))'); 31 | } else if (f === '==') { 32 | nstack.push('(' + n1 + ' === ' + n2 + ')'); 33 | } else if (f === '!=') { 34 | nstack.push('(' + n1 + ' !== ' + n2 + ')'); 35 | } else if (f === '[') { 36 | nstack.push(n1 + '[(' + n2 + ') | 0]'); 37 | } else { 38 | nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); 39 | } 40 | } else { 41 | if (f === '[') { 42 | nstack.push(n1 + '[' + n2 + ']'); 43 | } else { 44 | nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); 45 | } 46 | } 47 | } else if (type === IOP3) { 48 | n3 = nstack.pop(); 49 | n2 = nstack.pop(); 50 | n1 = nstack.pop(); 51 | f = item.value; 52 | if (f === '?') { 53 | nstack.push('(' + n1 + ' ? ' + n2 + ' : ' + n3 + ')'); 54 | } else { 55 | throw new Error('invalid Expression'); 56 | } 57 | } else if (type === IVAR || type === IVARNAME) { 58 | nstack.push(item.value); 59 | } else if (type === IOP1) { 60 | n1 = nstack.pop(); 61 | f = item.value; 62 | if (f === '-' || f === '+') { 63 | nstack.push('(' + f + n1 + ')'); 64 | } else if (toJS) { 65 | if (f === 'not') { 66 | nstack.push('(' + '!' + n1 + ')'); 67 | } else if (f === '!') { 68 | nstack.push('fac(' + n1 + ')'); 69 | } else { 70 | nstack.push(f + '(' + n1 + ')'); 71 | } 72 | } else if (f === '!') { 73 | nstack.push('(' + n1 + '!)'); 74 | } else { 75 | nstack.push('(' + f + ' ' + n1 + ')'); 76 | } 77 | } else if (type === IFUNCALL) { 78 | argCount = item.value; 79 | args = []; 80 | while (argCount-- > 0) { 81 | args.unshift(nstack.pop()); 82 | } 83 | f = nstack.pop(); 84 | nstack.push(f + '(' + args.join(', ') + ')'); 85 | } else if (type === IFUNDEF) { 86 | n2 = nstack.pop(); 87 | argCount = item.value; 88 | args = []; 89 | while (argCount-- > 0) { 90 | args.unshift(nstack.pop()); 91 | } 92 | n1 = nstack.pop(); 93 | if (toJS) { 94 | nstack.push('(' + n1 + ' = function(' + args.join(', ') + ') { return ' + n2 + ' })'); 95 | } else { 96 | nstack.push('(' + n1 + '(' + args.join(', ') + ') = ' + n2 + ')'); 97 | } 98 | } else if (type === IMEMBER) { 99 | n1 = nstack.pop(); 100 | nstack.push(n1 + '.' + item.value); 101 | } else if (type === IARRAY) { 102 | argCount = item.value; 103 | args = []; 104 | while (argCount-- > 0) { 105 | args.unshift(nstack.pop()); 106 | } 107 | nstack.push('[' + args.join(', ') + ']'); 108 | } else if (type === IEXPR) { 109 | nstack.push('(' + expressionToString(item.value, toJS) + ')'); 110 | } else if (type === IENDSTATEMENT) { 111 | // eslint-disable no-empty 112 | } else { 113 | throw new Error('invalid Expression'); 114 | } 115 | } 116 | if (nstack.length > 1) { 117 | if (toJS) { 118 | nstack = [ nstack.join(',') ]; 119 | } else { 120 | nstack = [ nstack.join(';') ]; 121 | } 122 | } 123 | return String(nstack[0]); 124 | } 125 | 126 | function escapeValue(v) { 127 | if (typeof v === 'string') { 128 | return JSON.stringify(v).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); 129 | } 130 | return v; 131 | } 132 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | import { TEOF } from './token'; 2 | import { TokenStream } from './token-stream'; 3 | import { ParserState } from './parser-state'; 4 | import { Expression } from './expression'; 5 | import { 6 | add, 7 | sub, 8 | mul, 9 | div, 10 | mod, 11 | concat, 12 | equal, 13 | notEqual, 14 | greaterThan, 15 | lessThan, 16 | greaterThanEqual, 17 | lessThanEqual, 18 | andOperator, 19 | orOperator, 20 | inOperator, 21 | sinh, 22 | cosh, 23 | tanh, 24 | asinh, 25 | acosh, 26 | atanh, 27 | log10, 28 | neg, 29 | not, 30 | trunc, 31 | random, 32 | factorial, 33 | gamma, 34 | stringOrArrayLength, 35 | hypot, 36 | condition, 37 | roundTo, 38 | setVar, 39 | arrayIndex, 40 | max, 41 | min, 42 | arrayMap, 43 | arrayFold, 44 | arrayFilter, 45 | stringOrArrayIndexOf, 46 | arrayJoin, 47 | sign, 48 | cbrt, 49 | expm1, 50 | log1p, 51 | log2, 52 | sum 53 | } from './functions'; 54 | 55 | export function Parser(options) { 56 | this.options = options || {}; 57 | this.unaryOps = { 58 | sin: Math.sin, 59 | cos: Math.cos, 60 | tan: Math.tan, 61 | asin: Math.asin, 62 | acos: Math.acos, 63 | atan: Math.atan, 64 | sinh: Math.sinh || sinh, 65 | cosh: Math.cosh || cosh, 66 | tanh: Math.tanh || tanh, 67 | asinh: Math.asinh || asinh, 68 | acosh: Math.acosh || acosh, 69 | atanh: Math.atanh || atanh, 70 | sqrt: Math.sqrt, 71 | cbrt: Math.cbrt || cbrt, 72 | log: Math.log, 73 | log2: Math.log2 || log2, 74 | ln: Math.log, 75 | lg: Math.log10 || log10, 76 | log10: Math.log10 || log10, 77 | expm1: Math.expm1 || expm1, 78 | log1p: Math.log1p || log1p, 79 | abs: Math.abs, 80 | ceil: Math.ceil, 81 | floor: Math.floor, 82 | round: Math.round, 83 | trunc: Math.trunc || trunc, 84 | '-': neg, 85 | '+': Number, 86 | exp: Math.exp, 87 | not: not, 88 | length: stringOrArrayLength, 89 | '!': factorial, 90 | sign: Math.sign || sign 91 | }; 92 | 93 | this.binaryOps = { 94 | '+': add, 95 | '-': sub, 96 | '*': mul, 97 | '/': div, 98 | '%': mod, 99 | '^': Math.pow, 100 | '||': concat, 101 | '==': equal, 102 | '!=': notEqual, 103 | '>': greaterThan, 104 | '<': lessThan, 105 | '>=': greaterThanEqual, 106 | '<=': lessThanEqual, 107 | and: andOperator, 108 | or: orOperator, 109 | 'in': inOperator, 110 | '=': setVar, 111 | '[': arrayIndex 112 | }; 113 | 114 | this.ternaryOps = { 115 | '?': condition 116 | }; 117 | 118 | this.functions = { 119 | random: random, 120 | fac: factorial, 121 | min: min, 122 | max: max, 123 | hypot: Math.hypot || hypot, 124 | pyt: Math.hypot || hypot, // backward compat 125 | pow: Math.pow, 126 | atan2: Math.atan2, 127 | 'if': condition, 128 | gamma: gamma, 129 | roundTo: roundTo, 130 | map: arrayMap, 131 | fold: arrayFold, 132 | filter: arrayFilter, 133 | indexOf: stringOrArrayIndexOf, 134 | join: arrayJoin, 135 | sum: sum 136 | }; 137 | 138 | this.consts = { 139 | E: Math.E, 140 | PI: Math.PI, 141 | 'true': true, 142 | 'false': false 143 | }; 144 | } 145 | 146 | Parser.prototype.parse = function (expr) { 147 | var instr = []; 148 | var parserState = new ParserState( 149 | this, 150 | new TokenStream(this, expr), 151 | { allowMemberAccess: this.options.allowMemberAccess } 152 | ); 153 | 154 | parserState.parseExpression(instr); 155 | parserState.expect(TEOF, 'EOF'); 156 | 157 | return new Expression(instr, this); 158 | }; 159 | 160 | Parser.prototype.evaluate = function (expr, variables) { 161 | return this.parse(expr).evaluate(variables); 162 | }; 163 | 164 | var sharedParser = new Parser(); 165 | 166 | Parser.parse = function (expr) { 167 | return sharedParser.parse(expr); 168 | }; 169 | 170 | Parser.evaluate = function (expr, variables) { 171 | return sharedParser.parse(expr).evaluate(variables); 172 | }; 173 | 174 | var optionNameMap = { 175 | '+': 'add', 176 | '-': 'subtract', 177 | '*': 'multiply', 178 | '/': 'divide', 179 | '%': 'remainder', 180 | '^': 'power', 181 | '!': 'factorial', 182 | '<': 'comparison', 183 | '>': 'comparison', 184 | '<=': 'comparison', 185 | '>=': 'comparison', 186 | '==': 'comparison', 187 | '!=': 'comparison', 188 | '||': 'concatenate', 189 | 'and': 'logical', 190 | 'or': 'logical', 191 | 'not': 'logical', 192 | '?': 'conditional', 193 | ':': 'conditional', 194 | '=': 'assignment', 195 | '[': 'array', 196 | '()=': 'fndef' 197 | }; 198 | 199 | function getOptionName(op) { 200 | return optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op; 201 | } 202 | 203 | Parser.prototype.isOperatorEnabled = function (op) { 204 | var optionName = getOptionName(op); 205 | var operators = this.options.operators || {}; 206 | 207 | return !(optionName in operators) || !!operators[optionName]; 208 | }; 209 | -------------------------------------------------------------------------------- /src/evaluate.js: -------------------------------------------------------------------------------- 1 | import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY } from './instruction'; 2 | 3 | export default function evaluate(tokens, expr, values) { 4 | var nstack = []; 5 | var n1, n2, n3; 6 | var f, args, argCount; 7 | 8 | if (isExpressionEvaluator(tokens)) { 9 | return resolveExpression(tokens, values); 10 | } 11 | 12 | var numTokens = tokens.length; 13 | 14 | for (var i = 0; i < numTokens; i++) { 15 | var item = tokens[i]; 16 | var type = item.type; 17 | if (type === INUMBER || type === IVARNAME) { 18 | nstack.push(item.value); 19 | } else if (type === IOP2) { 20 | n2 = nstack.pop(); 21 | n1 = nstack.pop(); 22 | if (item.value === 'and') { 23 | nstack.push(n1 ? !!evaluate(n2, expr, values) : false); 24 | } else if (item.value === 'or') { 25 | nstack.push(n1 ? true : !!evaluate(n2, expr, values)); 26 | } else if (item.value === '=') { 27 | f = expr.binaryOps[item.value]; 28 | nstack.push(f(n1, evaluate(n2, expr, values), values)); 29 | } else { 30 | f = expr.binaryOps[item.value]; 31 | nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values))); 32 | } 33 | } else if (type === IOP3) { 34 | n3 = nstack.pop(); 35 | n2 = nstack.pop(); 36 | n1 = nstack.pop(); 37 | if (item.value === '?') { 38 | nstack.push(evaluate(n1 ? n2 : n3, expr, values)); 39 | } else { 40 | f = expr.ternaryOps[item.value]; 41 | nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values))); 42 | } 43 | } else if (type === IVAR) { 44 | if (/^__proto__|prototype|constructor$/.test(item.value)) { 45 | throw new Error('prototype access detected'); 46 | } 47 | if (item.value in expr.functions) { 48 | nstack.push(expr.functions[item.value]); 49 | } else if (item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) { 50 | nstack.push(expr.unaryOps[item.value]); 51 | } else { 52 | var v = values[item.value]; 53 | if (v !== undefined) { 54 | nstack.push(v); 55 | } else { 56 | throw new Error('undefined variable: ' + item.value); 57 | } 58 | } 59 | } else if (type === IOP1) { 60 | n1 = nstack.pop(); 61 | f = expr.unaryOps[item.value]; 62 | nstack.push(f(resolveExpression(n1, values))); 63 | } else if (type === IFUNCALL) { 64 | argCount = item.value; 65 | args = []; 66 | while (argCount-- > 0) { 67 | args.unshift(resolveExpression(nstack.pop(), values)); 68 | } 69 | f = nstack.pop(); 70 | if (f.apply && f.call) { 71 | nstack.push(f.apply(undefined, args)); 72 | } else { 73 | throw new Error(f + ' is not a function'); 74 | } 75 | } else if (type === IFUNDEF) { 76 | // Create closure to keep references to arguments and expression 77 | nstack.push((function () { 78 | var n2 = nstack.pop(); 79 | var args = []; 80 | var argCount = item.value; 81 | while (argCount-- > 0) { 82 | args.unshift(nstack.pop()); 83 | } 84 | var n1 = nstack.pop(); 85 | var f = function () { 86 | var scope = Object.assign({}, values); 87 | for (var i = 0, len = args.length; i < len; i++) { 88 | scope[args[i]] = arguments[i]; 89 | } 90 | return evaluate(n2, expr, scope); 91 | }; 92 | // f.name = n1 93 | Object.defineProperty(f, 'name', { 94 | value: n1, 95 | writable: false 96 | }); 97 | values[n1] = f; 98 | return f; 99 | })()); 100 | } else if (type === IEXPR) { 101 | nstack.push(createExpressionEvaluator(item, expr, values)); 102 | } else if (type === IEXPREVAL) { 103 | nstack.push(item); 104 | } else if (type === IMEMBER) { 105 | n1 = nstack.pop(); 106 | nstack.push(n1[item.value]); 107 | } else if (type === IENDSTATEMENT) { 108 | nstack.pop(); 109 | } else if (type === IARRAY) { 110 | argCount = item.value; 111 | args = []; 112 | while (argCount-- > 0) { 113 | args.unshift(nstack.pop()); 114 | } 115 | nstack.push(args); 116 | } else { 117 | throw new Error('invalid Expression'); 118 | } 119 | } 120 | if (nstack.length > 1) { 121 | throw new Error('invalid Expression (parity)'); 122 | } 123 | // Explicitly return zero to avoid test issues caused by -0 124 | return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values); 125 | } 126 | 127 | function createExpressionEvaluator(token, expr, values) { 128 | if (isExpressionEvaluator(token)) return token; 129 | return { 130 | type: IEXPREVAL, 131 | value: function (scope) { 132 | return evaluate(token.value, expr, scope); 133 | } 134 | }; 135 | } 136 | 137 | function isExpressionEvaluator(n) { 138 | return n && n.type === IEXPREVAL; 139 | } 140 | 141 | function resolveExpression(n, values) { 142 | return isExpressionEvaluator(n) ? n.value(values) : n; 143 | } 144 | -------------------------------------------------------------------------------- /src/functions.js: -------------------------------------------------------------------------------- 1 | import contains from './contains'; 2 | 3 | export function add(a, b) { 4 | return Number(a) + Number(b); 5 | } 6 | 7 | export function sub(a, b) { 8 | return a - b; 9 | } 10 | 11 | export function mul(a, b) { 12 | return a * b; 13 | } 14 | 15 | export function div(a, b) { 16 | return a / b; 17 | } 18 | 19 | export function mod(a, b) { 20 | return a % b; 21 | } 22 | 23 | export function concat(a, b) { 24 | if (Array.isArray(a) && Array.isArray(b)) { 25 | return a.concat(b); 26 | } 27 | return '' + a + b; 28 | } 29 | 30 | export function equal(a, b) { 31 | return a === b; 32 | } 33 | 34 | export function notEqual(a, b) { 35 | return a !== b; 36 | } 37 | 38 | export function greaterThan(a, b) { 39 | return a > b; 40 | } 41 | 42 | export function lessThan(a, b) { 43 | return a < b; 44 | } 45 | 46 | export function greaterThanEqual(a, b) { 47 | return a >= b; 48 | } 49 | 50 | export function lessThanEqual(a, b) { 51 | return a <= b; 52 | } 53 | 54 | export function andOperator(a, b) { 55 | return Boolean(a && b); 56 | } 57 | 58 | export function orOperator(a, b) { 59 | return Boolean(a || b); 60 | } 61 | 62 | export function inOperator(a, b) { 63 | return contains(b, a); 64 | } 65 | 66 | export function sinh(a) { 67 | return ((Math.exp(a) - Math.exp(-a)) / 2); 68 | } 69 | 70 | export function cosh(a) { 71 | return ((Math.exp(a) + Math.exp(-a)) / 2); 72 | } 73 | 74 | export function tanh(a) { 75 | if (a === Infinity) return 1; 76 | if (a === -Infinity) return -1; 77 | return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a)); 78 | } 79 | 80 | export function asinh(a) { 81 | if (a === -Infinity) return a; 82 | return Math.log(a + Math.sqrt((a * a) + 1)); 83 | } 84 | 85 | export function acosh(a) { 86 | return Math.log(a + Math.sqrt((a * a) - 1)); 87 | } 88 | 89 | export function atanh(a) { 90 | return (Math.log((1 + a) / (1 - a)) / 2); 91 | } 92 | 93 | export function log10(a) { 94 | return Math.log(a) * Math.LOG10E; 95 | } 96 | 97 | export function neg(a) { 98 | return -a; 99 | } 100 | 101 | export function not(a) { 102 | return !a; 103 | } 104 | 105 | export function trunc(a) { 106 | return a < 0 ? Math.ceil(a) : Math.floor(a); 107 | } 108 | 109 | export function random(a) { 110 | return Math.random() * (a || 1); 111 | } 112 | 113 | export function factorial(a) { // a! 114 | return gamma(a + 1); 115 | } 116 | 117 | function isInteger(value) { 118 | return isFinite(value) && (value === Math.round(value)); 119 | } 120 | 121 | var GAMMA_G = 4.7421875; 122 | var GAMMA_P = [ 123 | 0.99999999999999709182, 124 | 57.156235665862923517, -59.597960355475491248, 125 | 14.136097974741747174, -0.49191381609762019978, 126 | 0.33994649984811888699e-4, 127 | 0.46523628927048575665e-4, -0.98374475304879564677e-4, 128 | 0.15808870322491248884e-3, -0.21026444172410488319e-3, 129 | 0.21743961811521264320e-3, -0.16431810653676389022e-3, 130 | 0.84418223983852743293e-4, -0.26190838401581408670e-4, 131 | 0.36899182659531622704e-5 132 | ]; 133 | 134 | // Gamma function from math.js 135 | export function gamma(n) { 136 | var t, x; 137 | 138 | if (isInteger(n)) { 139 | if (n <= 0) { 140 | return isFinite(n) ? Infinity : NaN; 141 | } 142 | 143 | if (n > 171) { 144 | return Infinity; // Will overflow 145 | } 146 | 147 | var value = n - 2; 148 | var res = n - 1; 149 | while (value > 1) { 150 | res *= value; 151 | value--; 152 | } 153 | 154 | if (res === 0) { 155 | res = 1; // 0! is per definition 1 156 | } 157 | 158 | return res; 159 | } 160 | 161 | if (n < 0.5) { 162 | return Math.PI / (Math.sin(Math.PI * n) * gamma(1 - n)); 163 | } 164 | 165 | if (n >= 171.35) { 166 | return Infinity; // will overflow 167 | } 168 | 169 | if (n > 85.0) { // Extended Stirling Approx 170 | var twoN = n * n; 171 | var threeN = twoN * n; 172 | var fourN = threeN * n; 173 | var fiveN = fourN * n; 174 | return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) * 175 | (1 + (1 / (12 * n)) + (1 / (288 * twoN)) - (139 / (51840 * threeN)) - 176 | (571 / (2488320 * fourN)) + (163879 / (209018880 * fiveN)) + 177 | (5246819 / (75246796800 * fiveN * n))); 178 | } 179 | 180 | --n; 181 | x = GAMMA_P[0]; 182 | for (var i = 1; i < GAMMA_P.length; ++i) { 183 | x += GAMMA_P[i] / (n + i); 184 | } 185 | 186 | t = n + GAMMA_G + 0.5; 187 | return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x; 188 | } 189 | 190 | export function stringOrArrayLength(s) { 191 | if (Array.isArray(s)) { 192 | return s.length; 193 | } 194 | return String(s).length; 195 | } 196 | 197 | export function hypot() { 198 | var sum = 0; 199 | var larg = 0; 200 | for (var i = 0; i < arguments.length; i++) { 201 | var arg = Math.abs(arguments[i]); 202 | var div; 203 | if (larg < arg) { 204 | div = larg / arg; 205 | sum = (sum * div * div) + 1; 206 | larg = arg; 207 | } else if (arg > 0) { 208 | div = arg / larg; 209 | sum += div * div; 210 | } else { 211 | sum += arg; 212 | } 213 | } 214 | return larg === Infinity ? Infinity : larg * Math.sqrt(sum); 215 | } 216 | 217 | export function condition(cond, yep, nope) { 218 | return cond ? yep : nope; 219 | } 220 | 221 | /** 222 | * Decimal adjustment of a number. 223 | * From @escopecz. 224 | * 225 | * @param {Number} value The number. 226 | * @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). 227 | * @return {Number} The adjusted value. 228 | */ 229 | export function roundTo(value, exp) { 230 | // If the exp is undefined or zero... 231 | if (typeof exp === 'undefined' || +exp === 0) { 232 | return Math.round(value); 233 | } 234 | value = +value; 235 | exp = -(+exp); 236 | // If the value is not a number or the exp is not an integer... 237 | if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { 238 | return NaN; 239 | } 240 | // Shift 241 | value = value.toString().split('e'); 242 | value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); 243 | // Shift back 244 | value = value.toString().split('e'); 245 | return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); 246 | } 247 | 248 | export function setVar(name, value, variables) { 249 | if (variables) variables[name] = value; 250 | return value; 251 | } 252 | 253 | export function arrayIndex(array, index) { 254 | return array[index | 0]; 255 | } 256 | 257 | export function max(array) { 258 | if (arguments.length === 1 && Array.isArray(array)) { 259 | return Math.max.apply(Math, array); 260 | } else { 261 | return Math.max.apply(Math, arguments); 262 | } 263 | } 264 | 265 | export function min(array) { 266 | if (arguments.length === 1 && Array.isArray(array)) { 267 | return Math.min.apply(Math, array); 268 | } else { 269 | return Math.min.apply(Math, arguments); 270 | } 271 | } 272 | 273 | export function arrayMap(f, a) { 274 | if (typeof f !== 'function') { 275 | throw new Error('First argument to map is not a function'); 276 | } 277 | if (!Array.isArray(a)) { 278 | throw new Error('Second argument to map is not an array'); 279 | } 280 | return a.map(function (x, i) { 281 | return f(x, i); 282 | }); 283 | } 284 | 285 | export function arrayFold(f, init, a) { 286 | if (typeof f !== 'function') { 287 | throw new Error('First argument to fold is not a function'); 288 | } 289 | if (!Array.isArray(a)) { 290 | throw new Error('Second argument to fold is not an array'); 291 | } 292 | return a.reduce(function (acc, x, i) { 293 | return f(acc, x, i); 294 | }, init); 295 | } 296 | 297 | export function arrayFilter(f, a) { 298 | if (typeof f !== 'function') { 299 | throw new Error('First argument to filter is not a function'); 300 | } 301 | if (!Array.isArray(a)) { 302 | throw new Error('Second argument to filter is not an array'); 303 | } 304 | return a.filter(function (x, i) { 305 | return f(x, i); 306 | }); 307 | } 308 | 309 | export function stringOrArrayIndexOf(target, s) { 310 | if (!(Array.isArray(s) || typeof s === 'string')) { 311 | throw new Error('Second argument to indexOf is not a string or array'); 312 | } 313 | 314 | return s.indexOf(target); 315 | } 316 | 317 | export function arrayJoin(sep, a) { 318 | if (!Array.isArray(a)) { 319 | throw new Error('Second argument to join is not an array'); 320 | } 321 | 322 | return a.join(sep); 323 | } 324 | 325 | export function sign(x) { 326 | return ((x > 0) - (x < 0)) || +x; 327 | } 328 | 329 | var ONE_THIRD = 1/3; 330 | export function cbrt(x) { 331 | return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD); 332 | } 333 | 334 | export function expm1(x) { 335 | return Math.exp(x) - 1; 336 | } 337 | 338 | export function log1p(x) { 339 | return Math.log(1 + x); 340 | } 341 | 342 | export function log2(x) { 343 | return Math.log(x) / Math.LN2; 344 | } 345 | 346 | export function sum(array) { 347 | if (!Array.isArray(array)) { 348 | throw new Error('Sum argument is not an array'); 349 | } 350 | 351 | return array.reduce(function (total, value) { 352 | return total + Number(value); 353 | }, 0); 354 | } 355 | -------------------------------------------------------------------------------- /src/parser-state.js: -------------------------------------------------------------------------------- 1 | import { TOP, TNUMBER, TSTRING, TPAREN, TBRACKET, TCOMMA, TNAME, TSEMICOLON, TEOF } from './token'; 2 | import { Instruction, INUMBER, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IMEMBER, IENDSTATEMENT, IARRAY, ternaryInstruction, binaryInstruction, unaryInstruction } from './instruction'; 3 | import contains from './contains'; 4 | 5 | export function ParserState(parser, tokenStream, options) { 6 | this.parser = parser; 7 | this.tokens = tokenStream; 8 | this.current = null; 9 | this.nextToken = null; 10 | this.next(); 11 | this.savedCurrent = null; 12 | this.savedNextToken = null; 13 | this.allowMemberAccess = options.allowMemberAccess !== false; 14 | } 15 | 16 | ParserState.prototype.next = function () { 17 | this.current = this.nextToken; 18 | return (this.nextToken = this.tokens.next()); 19 | }; 20 | 21 | ParserState.prototype.tokenMatches = function (token, value) { 22 | if (typeof value === 'undefined') { 23 | return true; 24 | } else if (Array.isArray(value)) { 25 | return contains(value, token.value); 26 | } else if (typeof value === 'function') { 27 | return value(token); 28 | } else { 29 | return token.value === value; 30 | } 31 | }; 32 | 33 | ParserState.prototype.save = function () { 34 | this.savedCurrent = this.current; 35 | this.savedNextToken = this.nextToken; 36 | this.tokens.save(); 37 | }; 38 | 39 | ParserState.prototype.restore = function () { 40 | this.tokens.restore(); 41 | this.current = this.savedCurrent; 42 | this.nextToken = this.savedNextToken; 43 | }; 44 | 45 | ParserState.prototype.accept = function (type, value) { 46 | if (this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) { 47 | this.next(); 48 | return true; 49 | } 50 | return false; 51 | }; 52 | 53 | ParserState.prototype.expect = function (type, value) { 54 | if (!this.accept(type, value)) { 55 | var coords = this.tokens.getCoordinates(); 56 | throw new Error('parse error [' + coords.line + ':' + coords.column + ']: Expected ' + (value || type)); 57 | } 58 | }; 59 | 60 | ParserState.prototype.parseAtom = function (instr) { 61 | var unaryOps = this.tokens.unaryOps; 62 | function isPrefixOperator(token) { 63 | return token.value in unaryOps; 64 | } 65 | 66 | if (this.accept(TNAME) || this.accept(TOP, isPrefixOperator)) { 67 | instr.push(new Instruction(IVAR, this.current.value)); 68 | } else if (this.accept(TNUMBER)) { 69 | instr.push(new Instruction(INUMBER, this.current.value)); 70 | } else if (this.accept(TSTRING)) { 71 | instr.push(new Instruction(INUMBER, this.current.value)); 72 | } else if (this.accept(TPAREN, '(')) { 73 | this.parseExpression(instr); 74 | this.expect(TPAREN, ')'); 75 | } else if (this.accept(TBRACKET, '[')) { 76 | if (this.accept(TBRACKET, ']')) { 77 | instr.push(new Instruction(IARRAY, 0)); 78 | } else { 79 | var argCount = this.parseArrayList(instr); 80 | instr.push(new Instruction(IARRAY, argCount)); 81 | } 82 | } else { 83 | throw new Error('unexpected ' + this.nextToken); 84 | } 85 | }; 86 | 87 | ParserState.prototype.parseExpression = function (instr) { 88 | var exprInstr = []; 89 | if (this.parseUntilEndStatement(instr, exprInstr)) { 90 | return; 91 | } 92 | this.parseVariableAssignmentExpression(exprInstr); 93 | if (this.parseUntilEndStatement(instr, exprInstr)) { 94 | return; 95 | } 96 | this.pushExpression(instr, exprInstr); 97 | }; 98 | 99 | ParserState.prototype.pushExpression = function (instr, exprInstr) { 100 | for (var i = 0, len = exprInstr.length; i < len; i++) { 101 | instr.push(exprInstr[i]); 102 | } 103 | }; 104 | 105 | ParserState.prototype.parseUntilEndStatement = function (instr, exprInstr) { 106 | if (!this.accept(TSEMICOLON)) return false; 107 | if (this.nextToken && this.nextToken.type !== TEOF && !(this.nextToken.type === TPAREN && this.nextToken.value === ')')) { 108 | exprInstr.push(new Instruction(IENDSTATEMENT)); 109 | } 110 | if (this.nextToken.type !== TEOF) { 111 | this.parseExpression(exprInstr); 112 | } 113 | instr.push(new Instruction(IEXPR, exprInstr)); 114 | return true; 115 | }; 116 | 117 | ParserState.prototype.parseArrayList = function (instr) { 118 | var argCount = 0; 119 | 120 | while (!this.accept(TBRACKET, ']')) { 121 | this.parseExpression(instr); 122 | ++argCount; 123 | while (this.accept(TCOMMA)) { 124 | this.parseExpression(instr); 125 | ++argCount; 126 | } 127 | } 128 | 129 | return argCount; 130 | }; 131 | 132 | ParserState.prototype.parseVariableAssignmentExpression = function (instr) { 133 | this.parseConditionalExpression(instr); 134 | while (this.accept(TOP, '=')) { 135 | var varName = instr.pop(); 136 | var varValue = []; 137 | var lastInstrIndex = instr.length - 1; 138 | if (varName.type === IFUNCALL) { 139 | if (!this.tokens.isOperatorEnabled('()=')) { 140 | throw new Error('function definition is not permitted'); 141 | } 142 | for (var i = 0, len = varName.value + 1; i < len; i++) { 143 | var index = lastInstrIndex - i; 144 | if (instr[index].type === IVAR) { 145 | instr[index] = new Instruction(IVARNAME, instr[index].value); 146 | } 147 | } 148 | this.parseVariableAssignmentExpression(varValue); 149 | instr.push(new Instruction(IEXPR, varValue)); 150 | instr.push(new Instruction(IFUNDEF, varName.value)); 151 | continue; 152 | } 153 | if (varName.type !== IVAR && varName.type !== IMEMBER) { 154 | throw new Error('expected variable for assignment'); 155 | } 156 | this.parseVariableAssignmentExpression(varValue); 157 | instr.push(new Instruction(IVARNAME, varName.value)); 158 | instr.push(new Instruction(IEXPR, varValue)); 159 | instr.push(binaryInstruction('=')); 160 | } 161 | }; 162 | 163 | ParserState.prototype.parseConditionalExpression = function (instr) { 164 | this.parseOrExpression(instr); 165 | while (this.accept(TOP, '?')) { 166 | var trueBranch = []; 167 | var falseBranch = []; 168 | this.parseConditionalExpression(trueBranch); 169 | this.expect(TOP, ':'); 170 | this.parseConditionalExpression(falseBranch); 171 | instr.push(new Instruction(IEXPR, trueBranch)); 172 | instr.push(new Instruction(IEXPR, falseBranch)); 173 | instr.push(ternaryInstruction('?')); 174 | } 175 | }; 176 | 177 | ParserState.prototype.parseOrExpression = function (instr) { 178 | this.parseAndExpression(instr); 179 | while (this.accept(TOP, 'or')) { 180 | var falseBranch = []; 181 | this.parseAndExpression(falseBranch); 182 | instr.push(new Instruction(IEXPR, falseBranch)); 183 | instr.push(binaryInstruction('or')); 184 | } 185 | }; 186 | 187 | ParserState.prototype.parseAndExpression = function (instr) { 188 | this.parseComparison(instr); 189 | while (this.accept(TOP, 'and')) { 190 | var trueBranch = []; 191 | this.parseComparison(trueBranch); 192 | instr.push(new Instruction(IEXPR, trueBranch)); 193 | instr.push(binaryInstruction('and')); 194 | } 195 | }; 196 | 197 | var COMPARISON_OPERATORS = ['==', '!=', '<', '<=', '>=', '>', 'in']; 198 | 199 | ParserState.prototype.parseComparison = function (instr) { 200 | this.parseAddSub(instr); 201 | while (this.accept(TOP, COMPARISON_OPERATORS)) { 202 | var op = this.current; 203 | this.parseAddSub(instr); 204 | instr.push(binaryInstruction(op.value)); 205 | } 206 | }; 207 | 208 | var ADD_SUB_OPERATORS = ['+', '-', '||']; 209 | 210 | ParserState.prototype.parseAddSub = function (instr) { 211 | this.parseTerm(instr); 212 | while (this.accept(TOP, ADD_SUB_OPERATORS)) { 213 | var op = this.current; 214 | this.parseTerm(instr); 215 | instr.push(binaryInstruction(op.value)); 216 | } 217 | }; 218 | 219 | var TERM_OPERATORS = ['*', '/', '%']; 220 | 221 | ParserState.prototype.parseTerm = function (instr) { 222 | this.parseFactor(instr); 223 | while (this.accept(TOP, TERM_OPERATORS)) { 224 | var op = this.current; 225 | this.parseFactor(instr); 226 | instr.push(binaryInstruction(op.value)); 227 | } 228 | }; 229 | 230 | ParserState.prototype.parseFactor = function (instr) { 231 | var unaryOps = this.tokens.unaryOps; 232 | function isPrefixOperator(token) { 233 | return token.value in unaryOps; 234 | } 235 | 236 | this.save(); 237 | if (this.accept(TOP, isPrefixOperator)) { 238 | if (this.current.value !== '-' && this.current.value !== '+') { 239 | if (this.nextToken.type === TPAREN && this.nextToken.value === '(') { 240 | this.restore(); 241 | this.parseExponential(instr); 242 | return; 243 | } else if (this.nextToken.type === TSEMICOLON || this.nextToken.type === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ')')) { 244 | this.restore(); 245 | this.parseAtom(instr); 246 | return; 247 | } 248 | } 249 | 250 | var op = this.current; 251 | this.parseFactor(instr); 252 | instr.push(unaryInstruction(op.value)); 253 | } else { 254 | this.parseExponential(instr); 255 | } 256 | }; 257 | 258 | ParserState.prototype.parseExponential = function (instr) { 259 | this.parsePostfixExpression(instr); 260 | while (this.accept(TOP, '^')) { 261 | this.parseFactor(instr); 262 | instr.push(binaryInstruction('^')); 263 | } 264 | }; 265 | 266 | ParserState.prototype.parsePostfixExpression = function (instr) { 267 | this.parseFunctionCall(instr); 268 | while (this.accept(TOP, '!')) { 269 | instr.push(unaryInstruction('!')); 270 | } 271 | }; 272 | 273 | ParserState.prototype.parseFunctionCall = function (instr) { 274 | var unaryOps = this.tokens.unaryOps; 275 | function isPrefixOperator(token) { 276 | return token.value in unaryOps; 277 | } 278 | 279 | if (this.accept(TOP, isPrefixOperator)) { 280 | var op = this.current; 281 | this.parseAtom(instr); 282 | instr.push(unaryInstruction(op.value)); 283 | } else { 284 | this.parseMemberExpression(instr); 285 | while (this.accept(TPAREN, '(')) { 286 | if (this.accept(TPAREN, ')')) { 287 | instr.push(new Instruction(IFUNCALL, 0)); 288 | } else { 289 | var argCount = this.parseArgumentList(instr); 290 | instr.push(new Instruction(IFUNCALL, argCount)); 291 | } 292 | } 293 | } 294 | }; 295 | 296 | ParserState.prototype.parseArgumentList = function (instr) { 297 | var argCount = 0; 298 | 299 | while (!this.accept(TPAREN, ')')) { 300 | this.parseExpression(instr); 301 | ++argCount; 302 | while (this.accept(TCOMMA)) { 303 | this.parseExpression(instr); 304 | ++argCount; 305 | } 306 | } 307 | 308 | return argCount; 309 | }; 310 | 311 | ParserState.prototype.parseMemberExpression = function (instr) { 312 | this.parseAtom(instr); 313 | while (this.accept(TOP, '.') || this.accept(TBRACKET, '[')) { 314 | var op = this.current; 315 | 316 | if (op.value === '.') { 317 | if (!this.allowMemberAccess) { 318 | throw new Error('unexpected ".", member access is not permitted'); 319 | } 320 | 321 | this.expect(TNAME); 322 | instr.push(new Instruction(IMEMBER, this.current.value)); 323 | } else if (op.value === '[') { 324 | if (!this.tokens.isOperatorEnabled('[')) { 325 | throw new Error('unexpected "[]", arrays are disabled'); 326 | } 327 | 328 | this.parseExpression(instr); 329 | this.expect(TBRACKET, ']'); 330 | instr.push(binaryInstruction('[')); 331 | } else { 332 | throw new Error('unexpected symbol: ' + op.value); 333 | } 334 | } 335 | }; 336 | -------------------------------------------------------------------------------- /src/token-stream.js: -------------------------------------------------------------------------------- 1 | import { Token, TEOF, TOP, TNUMBER, TSTRING, TPAREN, TBRACKET, TCOMMA, TNAME, TSEMICOLON } from './token'; 2 | 3 | export function TokenStream(parser, expression) { 4 | this.pos = 0; 5 | this.current = null; 6 | this.unaryOps = parser.unaryOps; 7 | this.binaryOps = parser.binaryOps; 8 | this.ternaryOps = parser.ternaryOps; 9 | this.consts = parser.consts; 10 | this.expression = expression; 11 | this.savedPosition = 0; 12 | this.savedCurrent = null; 13 | this.options = parser.options; 14 | this.parser = parser; 15 | } 16 | 17 | TokenStream.prototype.newToken = function (type, value, pos) { 18 | return new Token(type, value, pos != null ? pos : this.pos); 19 | }; 20 | 21 | TokenStream.prototype.save = function () { 22 | this.savedPosition = this.pos; 23 | this.savedCurrent = this.current; 24 | }; 25 | 26 | TokenStream.prototype.restore = function () { 27 | this.pos = this.savedPosition; 28 | this.current = this.savedCurrent; 29 | }; 30 | 31 | TokenStream.prototype.next = function () { 32 | if (this.pos >= this.expression.length) { 33 | return this.newToken(TEOF, 'EOF'); 34 | } 35 | 36 | if (this.isWhitespace() || this.isComment()) { 37 | return this.next(); 38 | } else if (this.isRadixInteger() || 39 | this.isNumber() || 40 | this.isOperator() || 41 | this.isString() || 42 | this.isParen() || 43 | this.isBracket() || 44 | this.isComma() || 45 | this.isSemicolon() || 46 | this.isNamedOp() || 47 | this.isConst() || 48 | this.isName()) { 49 | return this.current; 50 | } else { 51 | this.parseError('Unknown character "' + this.expression.charAt(this.pos) + '"'); 52 | } 53 | }; 54 | 55 | TokenStream.prototype.isString = function () { 56 | var r = false; 57 | var startPos = this.pos; 58 | var quote = this.expression.charAt(startPos); 59 | 60 | if (quote === '\'' || quote === '"') { 61 | var index = this.expression.indexOf(quote, startPos + 1); 62 | while (index >= 0 && this.pos < this.expression.length) { 63 | this.pos = index + 1; 64 | if (this.expression.charAt(index - 1) !== '\\') { 65 | var rawString = this.expression.substring(startPos + 1, index); 66 | this.current = this.newToken(TSTRING, this.unescape(rawString), startPos); 67 | r = true; 68 | break; 69 | } 70 | index = this.expression.indexOf(quote, index + 1); 71 | } 72 | } 73 | return r; 74 | }; 75 | 76 | TokenStream.prototype.isParen = function () { 77 | var c = this.expression.charAt(this.pos); 78 | if (c === '(' || c === ')') { 79 | this.current = this.newToken(TPAREN, c); 80 | this.pos++; 81 | return true; 82 | } 83 | return false; 84 | }; 85 | 86 | TokenStream.prototype.isBracket = function () { 87 | var c = this.expression.charAt(this.pos); 88 | if ((c === '[' || c === ']') && this.isOperatorEnabled('[')) { 89 | this.current = this.newToken(TBRACKET, c); 90 | this.pos++; 91 | return true; 92 | } 93 | return false; 94 | }; 95 | 96 | TokenStream.prototype.isComma = function () { 97 | var c = this.expression.charAt(this.pos); 98 | if (c === ',') { 99 | this.current = this.newToken(TCOMMA, ','); 100 | this.pos++; 101 | return true; 102 | } 103 | return false; 104 | }; 105 | 106 | TokenStream.prototype.isSemicolon = function () { 107 | var c = this.expression.charAt(this.pos); 108 | if (c === ';') { 109 | this.current = this.newToken(TSEMICOLON, ';'); 110 | this.pos++; 111 | return true; 112 | } 113 | return false; 114 | }; 115 | 116 | TokenStream.prototype.isConst = function () { 117 | var startPos = this.pos; 118 | var i = startPos; 119 | for (; i < this.expression.length; i++) { 120 | var c = this.expression.charAt(i); 121 | if (c.toUpperCase() === c.toLowerCase()) { 122 | if (i === this.pos || (c !== '_' && c !== '.' && (c < '0' || c > '9'))) { 123 | break; 124 | } 125 | } 126 | } 127 | if (i > startPos) { 128 | var str = this.expression.substring(startPos, i); 129 | if (str in this.consts) { 130 | this.current = this.newToken(TNUMBER, this.consts[str]); 131 | this.pos += str.length; 132 | return true; 133 | } 134 | } 135 | return false; 136 | }; 137 | 138 | TokenStream.prototype.isNamedOp = function () { 139 | var startPos = this.pos; 140 | var i = startPos; 141 | for (; i < this.expression.length; i++) { 142 | var c = this.expression.charAt(i); 143 | if (c.toUpperCase() === c.toLowerCase()) { 144 | if (i === this.pos || (c !== '_' && (c < '0' || c > '9'))) { 145 | break; 146 | } 147 | } 148 | } 149 | if (i > startPos) { 150 | var str = this.expression.substring(startPos, i); 151 | if (this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) { 152 | this.current = this.newToken(TOP, str); 153 | this.pos += str.length; 154 | return true; 155 | } 156 | } 157 | return false; 158 | }; 159 | 160 | TokenStream.prototype.isName = function () { 161 | var startPos = this.pos; 162 | var i = startPos; 163 | var hasLetter = false; 164 | for (; i < this.expression.length; i++) { 165 | var c = this.expression.charAt(i); 166 | if (c.toUpperCase() === c.toLowerCase()) { 167 | if (i === this.pos && (c === '$' || c === '_')) { 168 | if (c === '_') { 169 | hasLetter = true; 170 | } 171 | continue; 172 | } else if (i === this.pos || !hasLetter || (c !== '_' && (c < '0' || c > '9'))) { 173 | break; 174 | } 175 | } else { 176 | hasLetter = true; 177 | } 178 | } 179 | if (hasLetter) { 180 | var str = this.expression.substring(startPos, i); 181 | this.current = this.newToken(TNAME, str); 182 | this.pos += str.length; 183 | return true; 184 | } 185 | return false; 186 | }; 187 | 188 | TokenStream.prototype.isWhitespace = function () { 189 | var r = false; 190 | var c = this.expression.charAt(this.pos); 191 | while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { 192 | r = true; 193 | this.pos++; 194 | if (this.pos >= this.expression.length) { 195 | break; 196 | } 197 | c = this.expression.charAt(this.pos); 198 | } 199 | return r; 200 | }; 201 | 202 | var codePointPattern = /^[0-9a-f]{4}$/i; 203 | 204 | TokenStream.prototype.unescape = function (v) { 205 | var index = v.indexOf('\\'); 206 | if (index < 0) { 207 | return v; 208 | } 209 | 210 | var buffer = v.substring(0, index); 211 | while (index >= 0) { 212 | var c = v.charAt(++index); 213 | switch (c) { 214 | case '\'': 215 | buffer += '\''; 216 | break; 217 | case '"': 218 | buffer += '"'; 219 | break; 220 | case '\\': 221 | buffer += '\\'; 222 | break; 223 | case '/': 224 | buffer += '/'; 225 | break; 226 | case 'b': 227 | buffer += '\b'; 228 | break; 229 | case 'f': 230 | buffer += '\f'; 231 | break; 232 | case 'n': 233 | buffer += '\n'; 234 | break; 235 | case 'r': 236 | buffer += '\r'; 237 | break; 238 | case 't': 239 | buffer += '\t'; 240 | break; 241 | case 'u': 242 | // interpret the following 4 characters as the hex of the unicode code point 243 | var codePoint = v.substring(index + 1, index + 5); 244 | if (!codePointPattern.test(codePoint)) { 245 | this.parseError('Illegal escape sequence: \\u' + codePoint); 246 | } 247 | buffer += String.fromCharCode(parseInt(codePoint, 16)); 248 | index += 4; 249 | break; 250 | default: 251 | throw this.parseError('Illegal escape sequence: "\\' + c + '"'); 252 | } 253 | ++index; 254 | var backslash = v.indexOf('\\', index); 255 | buffer += v.substring(index, backslash < 0 ? v.length : backslash); 256 | index = backslash; 257 | } 258 | 259 | return buffer; 260 | }; 261 | 262 | TokenStream.prototype.isComment = function () { 263 | var c = this.expression.charAt(this.pos); 264 | if (c === '/' && this.expression.charAt(this.pos + 1) === '*') { 265 | this.pos = this.expression.indexOf('*/', this.pos) + 2; 266 | if (this.pos === 1) { 267 | this.pos = this.expression.length; 268 | } 269 | return true; 270 | } 271 | return false; 272 | }; 273 | 274 | TokenStream.prototype.isRadixInteger = function () { 275 | var pos = this.pos; 276 | 277 | if (pos >= this.expression.length - 2 || this.expression.charAt(pos) !== '0') { 278 | return false; 279 | } 280 | ++pos; 281 | 282 | var radix; 283 | var validDigit; 284 | if (this.expression.charAt(pos) === 'x') { 285 | radix = 16; 286 | validDigit = /^[0-9a-f]$/i; 287 | ++pos; 288 | } else if (this.expression.charAt(pos) === 'b') { 289 | radix = 2; 290 | validDigit = /^[01]$/i; 291 | ++pos; 292 | } else { 293 | return false; 294 | } 295 | 296 | var valid = false; 297 | var startPos = pos; 298 | 299 | while (pos < this.expression.length) { 300 | var c = this.expression.charAt(pos); 301 | if (validDigit.test(c)) { 302 | pos++; 303 | valid = true; 304 | } else { 305 | break; 306 | } 307 | } 308 | 309 | if (valid) { 310 | this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix)); 311 | this.pos = pos; 312 | } 313 | return valid; 314 | }; 315 | 316 | TokenStream.prototype.isNumber = function () { 317 | var valid = false; 318 | var pos = this.pos; 319 | var startPos = pos; 320 | var resetPos = pos; 321 | var foundDot = false; 322 | var foundDigits = false; 323 | var c; 324 | 325 | while (pos < this.expression.length) { 326 | c = this.expression.charAt(pos); 327 | if ((c >= '0' && c <= '9') || (!foundDot && c === '.')) { 328 | if (c === '.') { 329 | foundDot = true; 330 | } else { 331 | foundDigits = true; 332 | } 333 | pos++; 334 | valid = foundDigits; 335 | } else { 336 | break; 337 | } 338 | } 339 | 340 | if (valid) { 341 | resetPos = pos; 342 | } 343 | 344 | if (c === 'e' || c === 'E') { 345 | pos++; 346 | var acceptSign = true; 347 | var validExponent = false; 348 | while (pos < this.expression.length) { 349 | c = this.expression.charAt(pos); 350 | if (acceptSign && (c === '+' || c === '-')) { 351 | acceptSign = false; 352 | } else if (c >= '0' && c <= '9') { 353 | validExponent = true; 354 | acceptSign = false; 355 | } else { 356 | break; 357 | } 358 | pos++; 359 | } 360 | 361 | if (!validExponent) { 362 | pos = resetPos; 363 | } 364 | } 365 | 366 | if (valid) { 367 | this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos))); 368 | this.pos = pos; 369 | } else { 370 | this.pos = resetPos; 371 | } 372 | return valid; 373 | }; 374 | 375 | TokenStream.prototype.isOperator = function () { 376 | var startPos = this.pos; 377 | var c = this.expression.charAt(this.pos); 378 | 379 | if (c === '+' || c === '-' || c === '*' || c === '/' || c === '%' || c === '^' || c === '?' || c === ':' || c === '.') { 380 | this.current = this.newToken(TOP, c); 381 | } else if (c === '∙' || c === '•') { 382 | this.current = this.newToken(TOP, '*'); 383 | } else if (c === '>') { 384 | if (this.expression.charAt(this.pos + 1) === '=') { 385 | this.current = this.newToken(TOP, '>='); 386 | this.pos++; 387 | } else { 388 | this.current = this.newToken(TOP, '>'); 389 | } 390 | } else if (c === '<') { 391 | if (this.expression.charAt(this.pos + 1) === '=') { 392 | this.current = this.newToken(TOP, '<='); 393 | this.pos++; 394 | } else { 395 | this.current = this.newToken(TOP, '<'); 396 | } 397 | } else if (c === '|') { 398 | if (this.expression.charAt(this.pos + 1) === '|') { 399 | this.current = this.newToken(TOP, '||'); 400 | this.pos++; 401 | } else { 402 | return false; 403 | } 404 | } else if (c === '=') { 405 | if (this.expression.charAt(this.pos + 1) === '=') { 406 | this.current = this.newToken(TOP, '=='); 407 | this.pos++; 408 | } else { 409 | this.current = this.newToken(TOP, c); 410 | } 411 | } else if (c === '!') { 412 | if (this.expression.charAt(this.pos + 1) === '=') { 413 | this.current = this.newToken(TOP, '!='); 414 | this.pos++; 415 | } else { 416 | this.current = this.newToken(TOP, c); 417 | } 418 | } else { 419 | return false; 420 | } 421 | this.pos++; 422 | 423 | if (this.isOperatorEnabled(this.current.value)) { 424 | return true; 425 | } else { 426 | this.pos = startPos; 427 | return false; 428 | } 429 | }; 430 | 431 | TokenStream.prototype.isOperatorEnabled = function (op) { 432 | return this.parser.isOperatorEnabled(op); 433 | }; 434 | 435 | TokenStream.prototype.getCoordinates = function () { 436 | var line = 0; 437 | var column; 438 | var newline = -1; 439 | do { 440 | line++; 441 | column = this.pos - newline; 442 | newline = this.expression.indexOf('\n', newline + 1); 443 | } while (newline >= 0 && newline < this.pos); 444 | 445 | return { 446 | line: line, 447 | column: column 448 | }; 449 | }; 450 | 451 | TokenStream.prototype.parseError = function (msg) { 452 | var coords = this.getCoordinates(); 453 | throw new Error('parse error [' + coords.line + ':' + coords.column + ']: ' + msg); 454 | }; 455 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JavaScript Expression Evaluator 2 | =============================== 3 | 4 | [![npm](https://img.shields.io/npm/v/expr-eval.svg?maxAge=3600)](https://www.npmjs.com/package/expr-eval) 5 | [![CDNJS version](https://img.shields.io/cdnjs/v/expr-eval.svg?maxAge=3600)](https://cdnjs.com/libraries/expr-eval) 6 | [![Build Status](https://travis-ci.org/silentmatt/expr-eval.svg?branch=master)](https://travis-ci.org/silentmatt/expr-eval) 7 | 8 | Description 9 | ------------------------------------- 10 | 11 | Parses and evaluates mathematical expressions. It's a safer and more 12 | math-oriented alternative to using JavaScript’s `eval` function for mathematical 13 | expressions. 14 | 15 | It has built-in support for common math operators and functions. Additionally, 16 | you can add your own JavaScript functions. Expressions can be evaluated 17 | directly, or compiled into native JavaScript functions. 18 | 19 | Installation 20 | ------------------------------------- 21 | 22 | npm install expr-eval 23 | 24 | Basic Usage 25 | ------------------------------------- 26 | ```js 27 | const Parser = require('expr-eval').Parser; 28 | 29 | const parser = new Parser(); 30 | let expr = parser.parse('2 * x + 1'); 31 | console.log(expr.evaluate({ x: 3 })); // 7 32 | 33 | // or 34 | Parser.evaluate('6 * x', { x: 7 }) // 42 35 | ``` 36 | Documentation 37 | ------------------------------------- 38 | 39 | * [Parser](#parser) 40 | - [Parser()](#parser-1) 41 | - [parse(expression: string)](#parseexpression-string) 42 | - [Parser.parse(expression: string)](#parserparseexpression-string) 43 | - [Parser.evaluate(expression: string, variables?: object)](#parserevaluateexpression-string-variables-object) 44 | * [Expression](#expression) 45 | - [evaluate(variables?: object)](#evaluatevariables-object) 46 | - [substitute(variable: string, expression: Expression | string | number)](#substitutevariable-string-expression-expression--string--number) 47 | - [simplify(variables: object)](#simplifyvariables-object) 48 | - [variables(options?: object)](#variablesoptions-object) 49 | - [symbols(options?: object)](#symbolsoptions-object) 50 | - [toString()](#tostring) 51 | - [toJSFunction(parameters: array | string, variables?: object)](#tojsfunctionparameters-array--string-variables-object) 52 | * [Expression Syntax](#expression-syntax) 53 | - [Operator Precedence](#operator-precedence) 54 | - [Unary operators](#unary-operators) 55 | - [Array literals](#array-literals) 56 | - [Pre-defined functions](#pre-defined-functions) 57 | - [Custom JavaScript functions](#custom-javascript-functions) 58 | - [Constants](#constants) 59 | 60 | ### Parser ### 61 | 62 | Parser is the main class in the library. It has as single `parse` method, and 63 | "static" methods for parsing and evaluating expressions. 64 | 65 | #### Parser() 66 | 67 | Constructs a new `Parser` instance. 68 | 69 | The constructor takes an optional `options` parameter that allows you to enable or disable operators. 70 | 71 | For example, the following will create a `Parser` that does not allow comparison or logical operators, but does allow `in`: 72 | ```js 73 | const parser = new Parser({ 74 | operators: { 75 | // These default to true, but are included to be explicit 76 | add: true, 77 | concatenate: true, 78 | conditional: true, 79 | divide: true, 80 | factorial: true, 81 | multiply: true, 82 | power: true, 83 | remainder: true, 84 | subtract: true, 85 | 86 | // Disable and, or, not, <, ==, !=, etc. 87 | logical: false, 88 | comparison: false, 89 | 90 | // Disable 'in' and = operators 91 | 'in': false, 92 | assignment: false 93 | } 94 | }); 95 | ``` 96 | #### parse(expression: string) 97 | 98 | Convert a mathematical expression into an `Expression` object. 99 | 100 | #### Parser.parse(expression: string) 101 | 102 | Static equivalent of `new Parser().parse(expression)`. 103 | 104 | #### Parser.evaluate(expression: string, variables?: object) 105 | 106 | Parse and immediately evaluate an expression using the values and functions from 107 | the `variables` object. 108 | 109 | Parser.evaluate(expr, vars) is equivalent to calling 110 | Parser.parse(expr).evaluate(vars). 111 | 112 | ### Expression ### 113 | 114 | `Parser.parse(str)` returns an `Expression` object. `Expression`s are similar to 115 | JavaScript functions, i.e. they can be "called" with variables bound to 116 | passed-in values. In fact, they can even be converted into JavaScript 117 | functions. 118 | 119 | #### evaluate(variables?: object) 120 | 121 | Evaluate the expression, with variables bound to the values in {variables}. Each 122 | variable in the expression is bound to the corresponding member of the 123 | `variables` object. If there are unbound variables, `evaluate` will throw an 124 | exception. 125 | ```js 126 | js> expr = Parser.parse("2 ^ x"); 127 | (2^x) 128 | js> expr.evaluate({ x: 3 }); 129 | 8 130 | ``` 131 | #### substitute(variable: string, expression: Expression | string | number) 132 | 133 | Create a new `Expression` with the specified variable replaced with another 134 | expression. This is similar to function composition. If `expression` is a string 135 | or number, it will be parsed into an `Expression`. 136 | ```js 137 | js> expr = Parser.parse("2 * x + 1"); 138 | ((2*x)+1) 139 | js> expr.substitute("x", "4 * x"); 140 | ((2*(4*x))+1) 141 | js> expr2.evaluate({ x: 3 }); 142 | 25 143 | ``` 144 | #### simplify(variables: object) 145 | 146 | Simplify constant sub-expressions and replace variable references with literal 147 | values. This is basically a partial evaluation, that does as much of the 148 | calculation as it can with the provided variables. Function calls are not 149 | evaluated (except the built-in operator functions), since they may not be 150 | deterministic. 151 | 152 | Simplify is pretty simple. For example, it doesn’t know that addition and 153 | multiplication are associative, so `((2*(4*x))+1)` from the previous example 154 | cannot be simplified unless you provide a value for x. `2*4*x+1` can however, 155 | because it’s parsed as `(((2*4)*x)+1)`, so the `(2*4)` sub-expression will be 156 | replaced with "8", resulting in `((8*x)+1)`. 157 | ```js 158 | js> expr = Parser.parse("x * (y * atan(1))").simplify({ y: 4 }); 159 | (x*3.141592653589793) 160 | js> expr.evaluate({ x: 2 }); 161 | 6.283185307179586 162 | ``` 163 | #### variables(options?: object) 164 | 165 | Get an array of the unbound variables in the expression. 166 | ```js 167 | js> expr = Parser.parse("x * (y * atan(1))"); 168 | (x*(y*atan(1))) 169 | js> expr.variables(); 170 | x,y 171 | js> expr.simplify({ y: 4 }).variables(); 172 | x 173 | ``` 174 | By default, `variables` will return "top-level" objects, so for example, `Parser.parse(x.y.z).variables()` returns `['x']`. If you want to get the whole chain of object members, you can call it with `{ withMembers: true }`. So `Parser.parse(x.y.z).variables({ withMembers: true })` would return `['x.y.z']`. 175 | 176 | #### symbols(options?: object) 177 | 178 | Get an array of variables, including any built-in functions used in the 179 | expression. 180 | ```js 181 | js> expr = Parser.parse("min(x, y, z)"); 182 | (min(x, y, z)) 183 | js> expr.symbols(); 184 | min,x,y,z 185 | js> expr.simplify({ y: 4, z: 5 }).symbols(); 186 | min,x 187 | ``` 188 | Like `variables`, `symbols` accepts an option argument `{ withMembers: true }` to include object members. 189 | 190 | #### toString() 191 | 192 | Convert the expression to a string. `toString()` surrounds every sub-expression 193 | with parentheses (except literal values, variables, and function calls), so 194 | it’s useful for debugging precedence errors. 195 | 196 | #### toJSFunction(parameters: array | string, variables?: object) 197 | 198 | Convert an `Expression` object into a callable JavaScript function. `parameters` 199 | is an array of parameter names, or a string, with the names separated by commas. 200 | 201 | If the optional `variables` argument is provided, the expression will be 202 | simplified with variables bound to the supplied values. 203 | ```js 204 | js> expr = Parser.parse("x + y + z"); 205 | ((x + y) + z) 206 | js> f = expr.toJSFunction("x,y,z"); 207 | [Function] // function (x, y, z) { return x + y + z; }; 208 | js> f(1, 2, 3) 209 | 6 210 | js> f = expr.toJSFunction("y,z", { x: 100 }); 211 | [Function] // function (y, z) { return 100 + y + z; }; 212 | js> f(2, 3) 213 | 105 214 | ``` 215 | ### Expression Syntax ### 216 | 217 | The parser accepts a pretty basic grammar. It's similar to normal JavaScript 218 | expressions, but is more math-oriented. For example, the `^` operator is 219 | exponentiation, not xor. 220 | 221 | #### Operator Precedence 222 | 223 | Operator | Associativity | Description 224 | :----------------------- | :------------ | :---------- 225 | (...) | None | Grouping 226 | f(), x.y, a[i] | Left | Function call, property access, array indexing 227 | ! | Left | Factorial 228 | ^ | Right | Exponentiation 229 | +, -, not, sqrt, etc. | Right | Unary prefix operators (see below for the full list) 230 | \*, /, % | Left | Multiplication, division, remainder 231 | +, -, \|\| | Left | Addition, subtraction, array/list concatenation 232 | ==, !=, >=, <=, >, <, in | Left | Equals, not equals, etc. "in" means "is the left operand included in the right array operand?" 233 | and | Left | Logical AND 234 | or | Left | Logical OR 235 | x ? y : z | Right | Ternary conditional (if x then y else z) 236 | = | Right | Variable assignment 237 | ; | Left | Expression separator 238 | ```js 239 | const parser = new Parser({ 240 | operators: { 241 | 'in': true, 242 | 'assignment': true 243 | } 244 | }); 245 | // Now parser supports 'x in array' and 'y = 2*x' expressions 246 | ``` 247 | #### Unary operators 248 | 249 | The parser has several built-in "functions" that are actually unary operators. 250 | The primary difference between these and functions are that they can only accept 251 | exactly one argument, and parentheses are optional. With parentheses, they have 252 | the same precedence as function calls, but without parentheses, they keep their 253 | normal precedence (just below `^`). For example, `sin(x)^2` is equivalent to 254 | `(sin x)^2`, and `sin x^2` is equivalent to `sin(x^2)`. 255 | 256 | The unary `+` and `-` operators are an exception, and always have their normal 257 | precedence. 258 | 259 | Operator | Description 260 | :------- | :---------- 261 | -x | Negation 262 | +x | Unary plus. This converts it's operand to a number, but has no other effect. 263 | x! | Factorial (x * (x-1) * (x-2) * … * 2 * 1). gamma(x + 1) for non-integers. 264 | abs x | Absolute value (magnitude) of x 265 | acos x | Arc cosine of x (in radians) 266 | acosh x | Hyperbolic arc cosine of x (in radians) 267 | asin x | Arc sine of x (in radians) 268 | asinh x | Hyperbolic arc sine of x (in radians) 269 | atan x | Arc tangent of x (in radians) 270 | atanh x | Hyperbolic arc tangent of x (in radians) 271 | cbrt x | Cube root of x 272 | ceil x | Ceiling of x — the smallest integer that’s >= x 273 | cos x | Cosine of x (x is in radians) 274 | cosh x | Hyperbolic cosine of x (x is in radians) 275 | exp x | e^x (exponential/antilogarithm function with base e) 276 | expm1 x | e^x - 1 277 | floor x | Floor of x — the largest integer that’s <= x 278 | length x | String or array length of x 279 | ln x | Natural logarithm of x 280 | log x | Natural logarithm of x (synonym for ln, not base-10) 281 | log10 x | Base-10 logarithm of x 282 | log2 x | Base-2 logarithm of x 283 | log1p x | Natural logarithm of (1 + x) 284 | not x | Logical NOT operator 285 | round x | X, rounded to the nearest integer, using "grade-school rounding" 286 | sign x | Sign of x (-1, 0, or 1 for negative, zero, or positive respectively) 287 | sin x | Sine of x (x is in radians) 288 | sinh x | Hyperbolic sine of x (x is in radians) 289 | sqrt x | Square root of x. Result is NaN (Not a Number) if x is negative. 290 | tan x | Tangent of x (x is in radians) 291 | tanh x | Hyperbolic tangent of x (x is in radians) 292 | trunc x | Integral part of a X, looks like floor(x) unless for negative number 293 | 294 | #### Pre-defined functions 295 | 296 | Besides the "operator" functions, there are several pre-defined functions. You 297 | can provide your own, by binding variables to normal JavaScript functions. 298 | These are not evaluated by simplify. 299 | 300 | Function | Description 301 | :------------ | :---------- 302 | random(n) | Get a random number in the range [0, n). If n is zero, or not provided, it defaults to 1. 303 | fac(n) | n! (factorial of n: "n * (n-1) * (n-2) * … * 2 * 1") Deprecated. Use the ! operator instead. 304 | min(a,b,…) | Get the smallest (minimum) number in the list. 305 | max(a,b,…) | Get the largest (maximum) number in the list. 306 | hypot(a,b) | Hypotenuse, i.e. the square root of the sum of squares of its arguments. 307 | pyt(a, b) | Alias for hypot. 308 | pow(x, y) | Equivalent to x^y. For consistency with JavaScript's Math object. 309 | atan2(y, x) | Arc tangent of x/y. i.e. the angle between (0, 0) and (x, y) in radians. 310 | roundTo(x, n) | Rounds x to n places after the decimal point. 311 | map(f, a) | Array map: Pass each element of `a` the function `f`, and return an array of the results. 312 | fold(f, y, a) | Array fold: Fold/reduce array `a` into a single value, `y` by setting `y = f(y, x, index)` for each element `x` of the array. 313 | filter(f, a) | Array filter: Return an array containing only the values from `a` where `f(x, index)` is `true`. 314 | indexOf(x, a) | Return the first index of string or array `a` matching the value `x`, or `-1` if not found. 315 | join(sep, a) | Concatenate the elements of `a`, separated by `sep`. 316 | if(c, a, b) | Function form of c ? a : b. Note: This always evaluates both `a` and `b`, regardless of whether `c` is `true` or not. Use `c ? a : b` instead if there are side effects, or if evaluating the branches could be expensive. 317 | 318 | #### Array literals 319 | 320 | Arrays can be created by including the elements inside square `[]` brackets, separated by commas. For example: 321 | 322 | [ 1, 2, 3, 2+2, 10/2, 3! ] 323 | 324 | #### Function definitions 325 | 326 | You can define functions using the syntax `name(params) = expression`. When it's evaluated, the name will be added to the passed in scope as a function. You can call it later in the expression, or make it available to other expressions by re-using the same scope object. Functions can support multiple parameters, separated by commas. 327 | 328 | Examples: 329 | ```js 330 | square(x) = x*x 331 | add(a, b) = a + b 332 | factorial(x) = x < 2 ? 1 : x * factorial(x - 1) 333 | ``` 334 | #### Custom JavaScript functions 335 | 336 | If you need additional functions that aren't supported out of the box, you can easily add them in your own code. Instances of the `Parser` class have a property called `functions` that's simply an object with all the functions that are in scope. You can add, replace, or delete any of the properties to customize what's available in the expressions. For example: 337 | ```js 338 | const parser = new Parser(); 339 | 340 | // Add a new function 341 | parser.functions.customAddFunction = function (arg1, arg2) { 342 | return arg1 + arg2; 343 | }; 344 | 345 | // Remove the factorial function 346 | delete parser.functions.fac; 347 | 348 | parser.evaluate('customAddFunction(2, 4) == 6'); // true 349 | //parser.evaluate('fac(3)'); // This will fail 350 | ``` 351 | #### Constants 352 | 353 | The parser also includes a number of pre-defined constants that can be used in expressions. These are shown 354 | in the table below: 355 | 356 | Constant | Description 357 | :----------- | :---------- 358 | E | The value of `Math.E` from your JavaScript runtime 359 | PI | The value of `Math.PI` from your JavaScript runtime 360 | true | Logical `true` value 361 | false | Logical `false` value 362 | 363 | Pre-defined constants are stored in `parser.consts`. You can make changes to this property to customise the 364 | constants available to your expressions. For example: 365 | ```js 366 | const parser = new Parser(); 367 | parser.consts.R = 1.234; 368 | 369 | console.log(parser.parse('A+B/R').toString()); // ((A + B) / 1.234) 370 | ``` 371 | To disable the pre-defined constants, you can replace or delete `parser.consts`: 372 | ```js 373 | const parser = new Parser(); 374 | parser.consts = {}; 375 | ``` 376 | 377 | ### Tests ### 378 | 379 | 1. `cd` to the project directory 380 | 2. Install development dependencies: `npm install` 381 | 3. Run the tests: `npm test` 382 | -------------------------------------------------------------------------------- /test/functions.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | 'use strict'; 4 | 5 | var assert = require('assert'); 6 | var Parser = require('../dist/bundle').Parser; 7 | 8 | describe('Functions', function () { 9 | describe('roundTo()', function () { 10 | // Simple cases 11 | it('should handle roundTo(663)', function () { 12 | assert.strictEqual(Parser.evaluate('roundTo(663)'), 663); 13 | }); 14 | it('should handle roundTo(663, 0)', function () { 15 | assert.strictEqual(Parser.evaluate('roundTo(663, 0)'), 663); 16 | }); 17 | it('should handle roundTo(662.79)', function () { 18 | assert.strictEqual(Parser.evaluate('roundTo(662.79)'), 663); 19 | }); 20 | it('should handle roundTo(662.79, 1)', function () { 21 | assert.strictEqual(Parser.evaluate('roundTo(662.79, 1)'), 662.8); 22 | }); 23 | it('should handle roundTo(662.5, 1)', function () { 24 | assert.strictEqual(Parser.evaluate('roundTo(662.5, 1)'), 662.5); 25 | }); 26 | 27 | // Negative values and exponents 28 | it('should handle roundTo(54.1, -1)', function () { 29 | assert.strictEqual(Parser.evaluate('roundTo(54.1, -1)'), 50); 30 | }); 31 | it('should handle roundTo(-23.67, 1)', function () { 32 | assert.strictEqual(Parser.evaluate('roundTo(-23.67, 1)'), -23.7); 33 | }); 34 | it('should handle roundTo(-23.67, 1)', function () { 35 | assert.strictEqual(Parser.evaluate('roundTo(-23.67, 3)'), -23.670); 36 | }); 37 | 38 | // Big numbers 39 | it('should handle roundTo(3000000000000000000000000.1233, 1)', function () { 40 | assert.strictEqual(Parser.evaluate('roundTo(3000000000000000000000000.1233, 1)'), 3000000000000000000000000.1); 41 | }); 42 | it('should handle roundTo(-3000000000000000000000000.1233, 1)', function () { 43 | assert.strictEqual(Parser.evaluate('roundTo(-3000000000000000000000000.1233, 1)'), -3000000000000000000000000.1); 44 | }); 45 | it('should handle roundTo(3.12345e14, -13)', function () { 46 | assert.strictEqual(Parser.evaluate('roundTo(3.12345e14, -13)'), 3.1e14); 47 | }); 48 | 49 | // Known problems in other parsers 50 | // https://stackoverflow.com/a/12830454 51 | it('should handle roundTo(1.005, 2)', function () { 52 | assert.strictEqual(Parser.evaluate('roundTo(1.005, 2)'), 1.01); 53 | }); 54 | 55 | // Failure to parse (NaN) 56 | it('should make roundTo(-23, 1.2) NaN', function () { 57 | assert.ok(isNaN(Parser.evaluate('roundTo(-23, 1.2)'))); 58 | }); 59 | it('should make roundTo(-23, "blah") NaN', function () { 60 | assert.ok(isNaN(Parser.evaluate('roundTo(-23, "blah")'))); 61 | }); 62 | }); 63 | 64 | describe('random()', function () { 65 | it('should return a number from zero to 1', function () { 66 | var expr = Parser.parse('random()'); 67 | for (var i = 0; i < 1000; i++) { 68 | var x = expr.evaluate(); 69 | assert.ok(x >= 0 && x < 1); 70 | } 71 | }); 72 | 73 | it('should return different numbers', function () { 74 | var expr = Parser.parse('random()'); 75 | var distinct = {}; 76 | var sum = 0; 77 | for (var i = 0; i < 1000; i++) { 78 | var x = expr.evaluate(); 79 | sum += x; 80 | distinct[x] = true; 81 | } 82 | // Technically, these could fail but that should be extremely rare 83 | assert.strictEqual(Object.keys(distinct).length, 1000); 84 | assert.ok((sum / 1000 >= 0.4) && (sum / 1000 <= 0.6)); 85 | }); 86 | }); 87 | 88 | describe('fac(n)', function () { 89 | it('should return n!', function () { 90 | var parser = new Parser(); 91 | assert.strictEqual(parser.evaluate('fac(0)'), 1); 92 | assert.strictEqual(parser.evaluate('fac(1)'), 1); 93 | assert.strictEqual(parser.evaluate('fac(2)'), 2); 94 | assert.ok(Math.abs(parser.evaluate('fac(2.5)') - 3.323350970447843) <= 1e-14); 95 | assert.strictEqual(parser.evaluate('fac(3)'), 6); 96 | assert.strictEqual(parser.evaluate('fac(4)'), 24); 97 | assert.ok(Math.abs(parser.evaluate('fac(4.9)') - 101.27019121310335) <= 1e-12); 98 | assert.strictEqual(parser.evaluate('fac(5)'), 120); 99 | assert.strictEqual(parser.evaluate('fac(6)'), 720); 100 | assert.strictEqual(parser.evaluate('fac(7)'), 5040); 101 | assert.strictEqual(parser.evaluate('fac(21)'), 51090942171709440000); 102 | }); 103 | }); 104 | 105 | describe('min(a, b, ...)', function () { 106 | it('should return the smallest value', function () { 107 | var parser = new Parser(); 108 | assert.strictEqual(parser.evaluate('min()'), Infinity); 109 | assert.strictEqual(parser.evaluate('min([])'), Infinity); 110 | assert.strictEqual(parser.evaluate('min(1)'), 1); 111 | assert.strictEqual(parser.evaluate('min(1,2)'), 1); 112 | assert.strictEqual(parser.evaluate('min(2,1)'), 1); 113 | assert.strictEqual(parser.evaluate('min(2,1,0)'), 0); 114 | assert.strictEqual(parser.evaluate('min(4,3,2,1,0,1,2,3,4,-5,6)'), -5); 115 | assert.strictEqual(parser.evaluate('min([1,0,2,-4,8,-16,3.2])'), -16); 116 | }); 117 | }); 118 | 119 | describe('max(a, b, ...)', function () { 120 | it('should return the largest value', function () { 121 | var parser = new Parser(); 122 | assert.strictEqual(parser.evaluate('max()'), -Infinity); 123 | assert.strictEqual(parser.evaluate('max([])'), -Infinity); 124 | assert.strictEqual(parser.evaluate('max(1)'), 1); 125 | assert.strictEqual(parser.evaluate('max(1,2)'), 2); 126 | assert.strictEqual(parser.evaluate('max(2,1)'), 2); 127 | assert.strictEqual(parser.evaluate('max(2,1,0)'), 2); 128 | assert.strictEqual(parser.evaluate('max(4,3,2,1,0,1,2,3,4,-5,6)'), 6); 129 | assert.strictEqual(parser.evaluate('max([1,0,2,-4,8,-16,3.2])'), 8); 130 | }); 131 | }); 132 | 133 | describe('hypot(a, b, ...)', function () { 134 | var parser = new Parser(); 135 | 136 | it('should return the hypotenuse', function () { 137 | assert.strictEqual(parser.evaluate('hypot()'), 0); 138 | assert.strictEqual(parser.evaluate('hypot(3)'), 3); 139 | assert.strictEqual(parser.evaluate('hypot(3,4)'), 5); 140 | assert.strictEqual(parser.evaluate('hypot(4,3)'), 5); 141 | assert.strictEqual(parser.evaluate('hypot(2,3,4)'), 5.385164807134504); 142 | assert.strictEqual(parser.evaluate('hypot(1 / 0)'), Infinity); 143 | assert.strictEqual(parser.evaluate('hypot(-1 / 0)'), Infinity); 144 | assert.strictEqual(parser.evaluate('hypot(1, 2, 1 / 0)'), Infinity); 145 | }); 146 | 147 | it('should avoid overflowing', function () { 148 | assert.ok(Math.abs(parser.evaluate('hypot(10^200, 10^200)') - 1.4142135623730959e+200) <= 1e186); 149 | assert.ok(Math.abs(parser.evaluate('hypot(10^-200, 10^-200)') - 1.4142135623730944e-200) <= 1e186); 150 | assert.ok(Math.abs(parser.evaluate('hypot(10^100, 11^100, 12^100, 13^100)') - 2.4793352492856554e+111) <= 1e97); 151 | assert.strictEqual(parser.evaluate('hypot(x)', { x: Number.MAX_VALUE }), Number.MAX_VALUE); 152 | assert.strictEqual(parser.evaluate('hypot(x, 0)', { x: Number.MAX_VALUE }), Number.MAX_VALUE); 153 | }); 154 | }); 155 | 156 | describe('pow(x, y)', function () { 157 | it('should return x^y', function () { 158 | var parser = new Parser(); 159 | assert.strictEqual(parser.evaluate('pow(3,2)'), 9); 160 | assert.strictEqual(parser.evaluate('pow(E,1)'), Math.exp(1)); 161 | }); 162 | }); 163 | 164 | describe('atan2(y, x)', function () { 165 | it('should return atan(y / x)', function () { 166 | var parser = new Parser(); 167 | assert.strictEqual(parser.evaluate('atan2(90, 15)'), 1.4056476493802699); 168 | assert.strictEqual(parser.evaluate('atan2(15, 90)'), 0.16514867741462683); 169 | assert.strictEqual(parser.evaluate('atan2(0, 0)'), 0); 170 | assert.strictEqual(parser.evaluate('atan2(0, 1)'), 0); 171 | assert.strictEqual(parser.evaluate('atan2(1, 0)'), Math.PI / 2); 172 | assert.strictEqual(parser.evaluate('atan2(0, 1/-inf)', { inf: Infinity }), Math.PI); 173 | assert.strictEqual(parser.evaluate('atan2(1/-inf, 1/-inf)', { inf: Infinity }), -Math.PI); 174 | }); 175 | }); 176 | 177 | describe('if(p, t, f)', function () { 178 | it('if(1, 1, 0)', function () { 179 | assert.strictEqual(Parser.evaluate('if(1, 1, 0)'), 1); 180 | }); 181 | 182 | it('if(0, 1, 0)', function () { 183 | assert.strictEqual(Parser.evaluate('if(0, 1, 0)'), 0); 184 | }); 185 | 186 | it('if(1==1 or 2==1, 39, 0)', function () { 187 | assert.strictEqual(Parser.evaluate('if(1==1 or 2==1, 39, 0)'), 39); 188 | }); 189 | 190 | it('if(1==1 or 1==2, -4 + 8, 0)', function () { 191 | assert.strictEqual(Parser.evaluate('if(1==1 or 1==2, -4 + 8, 0)'), 4); 192 | }); 193 | 194 | it('if(3 && 6, if(45 > 5 * 11, 3 * 3, 2.4), 0)', function () { 195 | assert.strictEqual(Parser.evaluate('if(3 and 6, if(45 > 5 * 11, 3 * 3, 2.4), 0)'), 2.4); 196 | }); 197 | }); 198 | 199 | describe('gamma(x)', function () { 200 | var parser = new Parser(); 201 | 202 | it('returns exact answer for integers', function () { 203 | assert.strictEqual(parser.evaluate('gamma(0)'), Infinity); 204 | assert.strictEqual(parser.evaluate('gamma(1)'), 1); 205 | assert.strictEqual(parser.evaluate('gamma(2)'), 1); 206 | assert.strictEqual(parser.evaluate('gamma(3)'), 2); 207 | assert.strictEqual(parser.evaluate('gamma(4)'), 6); 208 | assert.strictEqual(parser.evaluate('gamma(5)'), 24); 209 | assert.strictEqual(parser.evaluate('gamma(6)'), 120); 210 | assert.strictEqual(parser.evaluate('gamma(7)'), 720); 211 | assert.strictEqual(parser.evaluate('gamma(8)'), 5040); 212 | assert.strictEqual(parser.evaluate('gamma(9)'), 40320); 213 | assert.strictEqual(parser.evaluate('gamma(10)'), 362880); 214 | assert.strictEqual(parser.evaluate('gamma(25)'), 6.204484017332394e+23); 215 | assert.strictEqual(parser.evaluate('gamma(50)'), 6.082818640342679e+62); 216 | assert.strictEqual(parser.evaluate('gamma(100)'), 9.332621544394415e+155); 217 | assert.strictEqual(parser.evaluate('gamma(170)'), 4.2690680090047056e+304); 218 | assert.strictEqual(parser.evaluate('gamma(171)'), 7.257415615308004e+306); 219 | assert.strictEqual(parser.evaluate('gamma(172)'), Infinity); 220 | }); 221 | 222 | it('returns approximation for fractions', function () { 223 | var delta = 1e-14; 224 | assert.strictEqual(parser.evaluate('gamma(-10)'), Infinity); 225 | assert.ok(Math.abs(parser.evaluate('gamma(-2.5)') - -0.9453087204829419) <= delta); 226 | assert.strictEqual(parser.evaluate('gamma(-2)'), Infinity); 227 | assert.ok(Math.abs(parser.evaluate('gamma(-1.5)') - 2.36327180120735) <= delta); 228 | assert.strictEqual(parser.evaluate('gamma(-1)'), Infinity); 229 | assert.ok(Math.abs(parser.evaluate('gamma(-0.75)') - -4.834146544295878) <= delta); 230 | assert.ok(Math.abs(parser.evaluate('gamma(-0.5)') - -3.54490770181103) <= delta); 231 | assert.ok(Math.abs(parser.evaluate('gamma(-0.25)') - -4.901666809860711) <= delta); 232 | assert.ok(Math.abs(parser.evaluate('gamma(0.25)') - 3.62560990822191) <= delta); 233 | assert.ok(Math.abs(parser.evaluate('gamma(0.5)') - 1.77245385090552) <= delta); 234 | assert.ok(Math.abs(parser.evaluate('gamma(0.75)') - 1.22541670246518) <= delta); 235 | assert.ok(Math.abs(parser.evaluate('gamma(1.5)') - 0.886226925452758) <= delta); 236 | assert.ok(Math.abs(parser.evaluate('gamma(84.9)') - 2.12678581783475e126) <= 1e113); 237 | assert.ok(Math.abs(parser.evaluate('gamma(85.1)') - 5.16530039995559e126) <= 1e114); 238 | assert.ok(Math.abs(parser.evaluate('gamma(98.6)') - 1.5043414559976e153) <= 1e140); 239 | assert.strictEqual(parser.evaluate('gamma(171.35)'), Infinity); 240 | assert.strictEqual(parser.evaluate('gamma(172)'), Infinity); 241 | assert.strictEqual(parser.evaluate('gamma(172.5)'), Infinity); 242 | }); 243 | 244 | it('handles NaN and infinity correctly', function () { 245 | assert.ok(isNaN(parser.evaluate('gamma(0/0)'))); 246 | assert.ok(isNaN(parser.evaluate('gamma()'))); 247 | assert.strictEqual(parser.evaluate('gamma(1/0)'), Infinity); 248 | assert.ok(isNaN(parser.evaluate('gamma(-1/0)'))); 249 | }); 250 | }); 251 | 252 | describe('map(f, a)', function () { 253 | it('should work on empty arrays', function () { 254 | var parser = new Parser(); 255 | assert.deepStrictEqual(parser.evaluate('map(random, [])'), []); 256 | }); 257 | 258 | it('should fail if first argument is not a function', function () { 259 | var parser = new Parser(); 260 | assert.throws(function () { parser.evaluate('map(4, [])'); }, /not a function/); 261 | }); 262 | 263 | it('should fail if second argument is not an array', function () { 264 | var parser = new Parser(); 265 | assert.throws(function () { parser.evaluate('map(random, 0)'); }, /not an array/); 266 | }); 267 | 268 | it('should call built-in functions', function () { 269 | var parser = new Parser(); 270 | assert.deepStrictEqual(parser.evaluate('map(sqrt, [0, 1, 16, 81])'), [ 0, 1, 4, 9 ]); 271 | assert.deepStrictEqual(parser.evaluate('map(max, [2, 2, 2, 2, 2, 2])'), [ 2, 2, 2, 3, 4, 5 ]); 272 | }); 273 | 274 | it('should call self-defined functions', function () { 275 | var parser = new Parser(); 276 | assert.deepStrictEqual(parser.evaluate('f(a) = a*a; map(f, [0, 1, 2, 3, 4])'), [ 0, 1, 4, 9, 16 ]); 277 | }); 278 | 279 | it('should call self-defined functions with index', function () { 280 | var parser = new Parser(); 281 | assert.deepStrictEqual(parser.evaluate('f(a, i) = a+i; map(f, [1,3,5,7,9])'), [ 1, 4, 7, 10, 13 ]); 282 | assert.deepStrictEqual(parser.evaluate('map(anon(a, i) = a+i, [1,3,5,7,9])'), [ 1, 4, 7, 10, 13 ]); 283 | assert.deepStrictEqual(parser.evaluate('f(a, i) = i; map(f, [1,3,5,7,9])'), [ 0, 1, 2, 3, 4 ]); 284 | }); 285 | }); 286 | 287 | describe('fold(f, init, array)', function () { 288 | it('should return the initial value on an empty array', function () { 289 | var parser = new Parser(); 290 | assert.strictEqual(parser.evaluate('fold(atan2, 15, [])'), 15); 291 | }); 292 | 293 | it('should fail if first argument is not a function', function () { 294 | var parser = new Parser(); 295 | assert.throws(function () { parser.evaluate('fold(4, 0, [])'); }, /not a function/); 296 | }); 297 | 298 | it('should fail if third argument is not an array', function () { 299 | var parser = new Parser(); 300 | assert.throws(function () { parser.evaluate('fold(random, 0, 5)'); }, /not an array/); 301 | }); 302 | 303 | it('should call built-in functions', function () { 304 | var parser = new Parser(); 305 | assert.strictEqual(parser.evaluate('fold(max, -1, [1, 3, 5, 4, 2, 0])'), 5); 306 | assert.strictEqual(parser.evaluate('fold(min, 10, [1, 3, 5, 4, 2, 0, -2, -1])'), -2); 307 | }); 308 | 309 | it('should call self-defined functions', function () { 310 | var parser = new Parser(); 311 | assert.strictEqual(parser.evaluate('f(a, b) = a*b; fold(f, 1, [1, 2, 3, 4, 5])'), 120); 312 | }); 313 | 314 | it('should call self-defined functions with index', function () { 315 | var parser = new Parser(); 316 | assert.strictEqual(parser.evaluate('f(a, b, i) = a*i + b; fold(f, 100, [1,3,5,7,9])'), 193); 317 | }); 318 | 319 | it('should start with the accumulator', function () { 320 | var parser = new Parser(); 321 | assert.strictEqual(parser.evaluate('f(a, b) = a*b; fold(f, 0, [1, 2, 3, 4, 5])'), 0); 322 | assert.strictEqual(parser.evaluate('f(a, b) = a*b; fold(f, 1, [1, 2, 3, 4, 5])'), 120); 323 | assert.strictEqual(parser.evaluate('f(a, b) = a*b; fold(f, 2, [1, 2, 3, 4, 5])'), 240); 324 | assert.strictEqual(parser.evaluate('f(a, b) = a*b; fold(f, 3, [1, 2, 3, 4, 5])'), 360); 325 | }); 326 | }); 327 | 328 | describe('filter(f, array)', function () { 329 | it('should work on an empty array', function () { 330 | var parser = new Parser(); 331 | assert.deepStrictEqual(parser.evaluate('filter(random, [])'), []); 332 | }); 333 | 334 | it('should fail if first argument is not a function', function () { 335 | var parser = new Parser(); 336 | assert.throws(function () { parser.evaluate('filter(4, [])'); }, /not a function/); 337 | }); 338 | 339 | it('should fail if second argument is not an array', function () { 340 | var parser = new Parser(); 341 | assert.throws(function () { parser.evaluate('filter(random, 5)'); }, /not an array/); 342 | }); 343 | 344 | it('should call built-in functions', function () { 345 | var parser = new Parser(); 346 | assert.deepStrictEqual(parser.evaluate('filter(not, [1, 0, false, true, 2, ""])'), [ 0, false, '' ]); 347 | }); 348 | 349 | it('should call self-defined functions', function () { 350 | var parser = new Parser(); 351 | assert.deepStrictEqual(parser.evaluate('f(x) = x > 2; filter(f, [1, 2, 0, 3, -1, 4])'), [ 3, 4 ]); 352 | assert.deepStrictEqual(parser.evaluate('f(x) = x > 2; filter(f, [1, 2, 0, 1.9, -1, -4])'), []); 353 | }); 354 | 355 | it('should call self-defined functions with index', function () { 356 | var parser = new Parser(); 357 | assert.deepStrictEqual(parser.evaluate('f(a, i) = a <= i; filter(f, [1,0,5,3,2])'), [ 0, 3, 2 ]); 358 | assert.deepStrictEqual(parser.evaluate('f(a, i) = i > 3; filter(f, [9,0,5,6,1,2,3,4])'), [ 1, 2, 3, 4 ]); 359 | }); 360 | }); 361 | 362 | describe('indexOf(target, array)', function () { 363 | it('should return -1 an empty array', function () { 364 | var parser = new Parser(); 365 | assert.strictEqual(parser.evaluate('indexOf(1, [])'), -1); 366 | }); 367 | 368 | it('should fail if second argument is not an array or string', function () { 369 | var parser = new Parser(); 370 | assert.throws(function () { parser.evaluate('indexOf(5, 5)'); }, /not a string or array/); 371 | }); 372 | 373 | it('should find values in the array', function () { 374 | var parser = new Parser(); 375 | assert.strictEqual(parser.evaluate('indexOf(1, [1,0,5,3,2])'), 0); 376 | assert.strictEqual(parser.evaluate('indexOf(0, [1,0,5,3,2])'), 1); 377 | assert.strictEqual(parser.evaluate('indexOf(5, [1,0,5,3,2])'), 2); 378 | assert.strictEqual(parser.evaluate('indexOf(3, [1,0,5,3,2])'), 3); 379 | assert.strictEqual(parser.evaluate('indexOf(2, [1,0,5,3,2])'), 4); 380 | }); 381 | 382 | it('should find the first matching value in the array', function () { 383 | var parser = new Parser(); 384 | assert.strictEqual(parser.evaluate('indexOf(5, [5,0,5,3,2])'), 0); 385 | }); 386 | 387 | it('should return -1 for no match', function () { 388 | var parser = new Parser(); 389 | assert.strictEqual(parser.evaluate('indexOf(2.5, [1,0,5,3,2])'), -1); 390 | assert.strictEqual(parser.evaluate('indexOf("5", [1,0,5,3,2])'), -1); 391 | }); 392 | }); 393 | 394 | describe('indexOf(target, string)', function () { 395 | it('return -1 for indexOf("x", "")', function () { 396 | var parser = new Parser(); 397 | assert.strictEqual(parser.evaluate('indexOf("a", "")'), -1); 398 | }); 399 | 400 | it('return 0 for indexOf("", *)', function () { 401 | var parser = new Parser(); 402 | assert.strictEqual(parser.evaluate('indexOf("", "")'), 0); 403 | assert.strictEqual(parser.evaluate('indexOf("", "a")'), 0); 404 | assert.strictEqual(parser.evaluate('indexOf("", "foobar")'), 0); 405 | }); 406 | 407 | it('should find substrings in the string', function () { 408 | var parser = new Parser(); 409 | assert.strictEqual(parser.evaluate('indexOf("b", "bafdc")'), 0); 410 | assert.strictEqual(parser.evaluate('indexOf("a", "bafdc")'), 1); 411 | assert.strictEqual(parser.evaluate('indexOf("f", "bafdc")'), 2); 412 | assert.strictEqual(parser.evaluate('indexOf("d", "bafdc")'), 3); 413 | assert.strictEqual(parser.evaluate('indexOf("c", "bafdc")'), 4); 414 | 415 | assert.strictEqual(parser.evaluate('indexOf("ba", "bafdc")'), 0); 416 | assert.strictEqual(parser.evaluate('indexOf("afd", "bafdc")'), 1); 417 | assert.strictEqual(parser.evaluate('indexOf("fdc", "bafdc")'), 2); 418 | assert.strictEqual(parser.evaluate('indexOf("dc", "bafdc")'), 3); 419 | assert.strictEqual(parser.evaluate('indexOf("c", "bafdc")'), 4); 420 | 421 | assert.strictEqual(parser.evaluate('indexOf("dc", "dbafdc")'), 4); 422 | }); 423 | 424 | it('should find the first matching substring in the string', function () { 425 | var parser = new Parser(); 426 | assert.strictEqual(parser.evaluate('indexOf("c", "abcabcabc")'), 2); 427 | assert.strictEqual(parser.evaluate('indexOf("ca", "abcabcabc")'), 2); 428 | }); 429 | 430 | it('should find the entire string', function () { 431 | var parser = new Parser(); 432 | assert.strictEqual(parser.evaluate('indexOf("abcabcabc", "abcabcabc")'), 0); 433 | }); 434 | 435 | it('should return -1 for no match', function () { 436 | var parser = new Parser(); 437 | assert.strictEqual(parser.evaluate('indexOf("x", "abcdefg")'), -1); 438 | assert.strictEqual(parser.evaluate('indexOf("abd", "abcdefg")'), -1); 439 | }); 440 | }); 441 | 442 | describe('join(sep, array)', function () { 443 | it('should fail if second argument is not an array', function () { 444 | var parser = new Parser(); 445 | assert.throws(function () { parser.evaluate('join("x", "y")'); }, /not an array/); 446 | }); 447 | 448 | it('should return an empty string on an empty array', function () { 449 | var parser = new Parser(); 450 | assert.strictEqual(parser.evaluate('join("x", [])'), ''); 451 | }); 452 | 453 | it('should work on a single-element array', function () { 454 | var parser = new Parser(); 455 | assert.strictEqual(parser.evaluate('join("x", ["a"])'), 'a'); 456 | assert.strictEqual(parser.evaluate('join("x", [5])'), '5'); 457 | }); 458 | 459 | it('should work on a multi-element arrays', function () { 460 | var parser = new Parser(); 461 | assert.strictEqual(parser.evaluate('join("x", ["a", "b", "c", 4])'), 'axbxcx4'); 462 | assert.strictEqual(parser.evaluate('join(", ", [1, 2])'), '1, 2'); 463 | assert.strictEqual(parser.evaluate('join("", [1, 2, 3])'), '123'); 464 | }); 465 | }); 466 | 467 | describe('sum(array)', function () { 468 | it('should fail if the argument is not an array', function () { 469 | var parser = new Parser(); 470 | assert.throws(function () { parser.evaluate('sum(1)'); }, /not an array/); 471 | }); 472 | 473 | it('should return zero with an empty array', function () { 474 | var parser = new Parser(); 475 | assert.strictEqual(parser.evaluate('sum([])'), 0); 476 | }); 477 | 478 | it('should work on a single-element array', function () { 479 | var parser = new Parser(); 480 | assert.strictEqual(parser.evaluate('sum([1])'), 1); 481 | }); 482 | 483 | it('should work on a multi-element array', function () { 484 | var parser = new Parser(); 485 | assert.strictEqual(parser.evaluate('sum([1, 2])'), 3); 486 | }); 487 | }); 488 | }); 489 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | 'use strict'; 4 | 5 | var assert = require('assert'); 6 | var Parser = require('../dist/bundle').Parser; 7 | 8 | describe('Parser', function () { 9 | [ 10 | { name: 'normal parse()', parser: new Parser() }, 11 | { name: 'disallowing member access', parser: new Parser({ allowMemberAccess: false }) } 12 | ].forEach(function (tcase) { 13 | var parser = tcase.parser; 14 | describe(tcase.name, function () { 15 | it('should skip comments', function () { 16 | assert.strictEqual(parser.evaluate('2/* comment */+/* another comment */3'), 5); 17 | assert.strictEqual(parser.evaluate('2/* comment *///* another comment */3'), 2 / 3); 18 | assert.strictEqual(parser.evaluate('/* comment at the beginning */2 + 3/* unterminated comment'), 5); 19 | assert.strictEqual(parser.evaluate('2 +/* comment\n with\n multiple\n lines */3'), 5); 20 | }); 21 | 22 | it('should ignore whitespace', function () { 23 | assert.strictEqual(parser.evaluate(' 3\r + \n \t 4 '), 7); 24 | }); 25 | 26 | it('should accept variables starting with E', function () { 27 | assert.strictEqual(parser.parse('2 * ERGONOMIC').evaluate({ ERGONOMIC: 1000 }), 2000); 28 | }); 29 | 30 | it('should accept variables starting with PI', function () { 31 | assert.strictEqual(parser.parse('1 / PITTSBURGH').evaluate({ PITTSBURGH: 2 }), 0.5); 32 | }); 33 | 34 | it('should fail on empty parentheses', function () { 35 | assert.throws(function () { parser.parse('5/()'); }, Error); 36 | }); 37 | 38 | it('should fail on 5/', function () { 39 | assert.throws(function () { parser.parse('5/'); }, Error); 40 | }); 41 | 42 | it('should parse numbers', function () { 43 | assert.strictEqual(parser.evaluate('123'), 123); 44 | assert.strictEqual(parser.evaluate('123.'), 123); 45 | assert.strictEqual(parser.evaluate('123.456'), 123.456); 46 | assert.strictEqual(parser.evaluate('.456'), 0.456); 47 | assert.strictEqual(parser.evaluate('0.456'), 0.456); 48 | assert.strictEqual(parser.evaluate('0.'), 0); 49 | assert.strictEqual(parser.evaluate('.0'), 0); 50 | assert.strictEqual(parser.evaluate('123.+3'), 126); 51 | assert.strictEqual(parser.evaluate('2/123'), 2 / 123); 52 | }); 53 | 54 | it('should parse numbers using scientific notation', function () { 55 | assert.strictEqual(parser.evaluate('123e2'), 12300); 56 | assert.strictEqual(parser.evaluate('123E2'), 12300); 57 | assert.strictEqual(parser.evaluate('123e12'), 123000000000000); 58 | assert.strictEqual(parser.evaluate('123e+12'), 123000000000000); 59 | assert.strictEqual(parser.evaluate('123E+12'), 123000000000000); 60 | assert.strictEqual(parser.evaluate('123e-12'), 0.000000000123); 61 | assert.strictEqual(parser.evaluate('123E-12'), 0.000000000123); 62 | assert.strictEqual(parser.evaluate('1.7e308'), 1.7e308); 63 | assert.strictEqual(parser.evaluate('1.7e-308'), 1.7e-308); 64 | assert.strictEqual(parser.evaluate('123.e3'), 123000); 65 | assert.strictEqual(parser.evaluate('123.456e+1'), 1234.56); 66 | assert.strictEqual(parser.evaluate('.456e-3'), 0.000456); 67 | assert.strictEqual(parser.evaluate('0.456'), 0.456); 68 | assert.strictEqual(parser.evaluate('0e3'), 0); 69 | assert.strictEqual(parser.evaluate('0e-3'), 0); 70 | assert.strictEqual(parser.evaluate('0e+3'), 0); 71 | assert.strictEqual(parser.evaluate('.0e+3'), 0); 72 | assert.strictEqual(parser.evaluate('.0e-3'), 0); 73 | assert.strictEqual(parser.evaluate('123e5+4'), 12300004); 74 | assert.strictEqual(parser.evaluate('123e+5+4'), 12300004); 75 | assert.strictEqual(parser.evaluate('123e-5+4'), 4.00123); 76 | assert.strictEqual(parser.evaluate('123e0'), 123); 77 | assert.strictEqual(parser.evaluate('123e01'), 1230); 78 | assert.strictEqual(parser.evaluate('123e+00000000002'), 12300); 79 | assert.strictEqual(parser.evaluate('123e-00000000002'), 1.23); 80 | assert.strictEqual(parser.evaluate('e1', { e1: 42 }), 42); 81 | assert.strictEqual(parser.evaluate('e+1', { e: 12 }), 13); 82 | }); 83 | 84 | it('should fail on invalid numbers', function () { 85 | assert.throws(function () { parser.parse('123..'); }, Error); 86 | assert.throws(function () { parser.parse('0..123'); }, Error); 87 | assert.throws(function () { parser.parse('0..'); }, Error); 88 | assert.throws(function () { parser.parse('.0.'); }, Error); 89 | assert.throws(function () { parser.parse('.'); }, Error); 90 | assert.throws(function () { parser.parse('1.23e'); }, Error); 91 | assert.throws(function () { parser.parse('1.23e+'); }, Error); 92 | assert.throws(function () { parser.parse('1.23e-'); }, Error); 93 | assert.throws(function () { parser.parse('1.23e++4'); }, Error); 94 | assert.throws(function () { parser.parse('1.23e--4'); }, Error); 95 | assert.throws(function () { parser.parse('1.23e+-4'); }, Error); 96 | assert.throws(function () { parser.parse('1.23e4-'); }, Error); 97 | assert.throws(function () { parser.parse('1.23ee4'); }, Error); 98 | assert.throws(function () { parser.parse('1.23ee.4'); }, Error); 99 | assert.throws(function () { parser.parse('1.23e4.0'); }, Error); 100 | assert.throws(function () { parser.parse('123e.4'); }, Error); 101 | }); 102 | 103 | it('should parse hexadecimal integers correctly', function () { 104 | assert.strictEqual(parser.evaluate('0x0'), 0x0); 105 | assert.strictEqual(parser.evaluate('0x1'), 0x1); 106 | assert.strictEqual(parser.evaluate('0xA'), 0xA); 107 | assert.strictEqual(parser.evaluate('0xF'), 0xF); 108 | assert.strictEqual(parser.evaluate('0x123'), 0x123); 109 | assert.strictEqual(parser.evaluate('0x123ABCD'), 0x123ABCD); 110 | assert.strictEqual(parser.evaluate('0xDEADBEEF'), 0xDEADBEEF); 111 | assert.strictEqual(parser.evaluate('0xdeadbeef'), 0xdeadbeef); 112 | assert.strictEqual(parser.evaluate('0xABCDEF'), 0xABCDEF); 113 | assert.strictEqual(parser.evaluate('0xabcdef'), 0xABCDEF); 114 | assert.strictEqual(parser.evaluate('0x1e+4'), 0x1e + 4); 115 | assert.strictEqual(parser.evaluate('0x1E+4'), 0x1e + 4); 116 | assert.strictEqual(parser.evaluate('0x1e-4'), 0x1e - 4); 117 | assert.strictEqual(parser.evaluate('0x1E-4'), 0x1e - 4); 118 | assert.strictEqual(parser.evaluate('0xFFFFFFFF'), Math.pow(2, 32) - 1); 119 | assert.strictEqual(parser.evaluate('0x100000000'), Math.pow(2, 32)); 120 | assert.strictEqual(parser.evaluate('0x1FFFFFFFFFFFFF'), Math.pow(2, 53) - 1); 121 | assert.strictEqual(parser.evaluate('0x20000000000000'), Math.pow(2, 53)); 122 | }); 123 | 124 | it('should parse binary integers correctly', function () { 125 | assert.strictEqual(parser.evaluate('0b0'), 0); 126 | assert.strictEqual(parser.evaluate('0b1'), 1); 127 | assert.strictEqual(parser.evaluate('0b01'), 1); 128 | assert.strictEqual(parser.evaluate('0b10'), 2); 129 | assert.strictEqual(parser.evaluate('0b100'), 4); 130 | assert.strictEqual(parser.evaluate('0b101'), 5); 131 | assert.strictEqual(parser.evaluate('0b10101'), 21); 132 | assert.strictEqual(parser.evaluate('0b10111'), 23); 133 | assert.strictEqual(parser.evaluate('0b11111'), 31); 134 | assert.strictEqual(parser.evaluate('0b11111111111111111111111111111111'), Math.pow(2, 32) - 1); 135 | assert.strictEqual(parser.evaluate('0b100000000000000000000000000000000'), Math.pow(2, 32)); 136 | assert.strictEqual(parser.evaluate('0b11111111111111111111111111111111111111111111111111111'), Math.pow(2, 53) - 1); 137 | assert.strictEqual(parser.evaluate('0b100000000000000000000000000000000000000000000000000000'), Math.pow(2, 53)); 138 | }); 139 | 140 | it('should fail on invalid hexadecimal numbers', function () { 141 | assert.throws(function () { parser.parse('0x'); }, Error); 142 | assert.throws(function () { parser.parse('0x + 1'); }, Error); 143 | assert.throws(function () { parser.parse('0x1.23'); }, Error); 144 | assert.throws(function () { parser.parse('0xG'); }, Error); 145 | assert.throws(function () { parser.parse('0xx0'); }, Error); 146 | assert.throws(function () { parser.parse('0x1g'); }, Error); 147 | assert.throws(function () { parser.parse('1x0'); }, Error); 148 | }); 149 | 150 | it('should fail on invalid binary numbers', function () { 151 | assert.throws(function () { parser.parse('0b'); }, Error); 152 | assert.throws(function () { parser.parse('0b + 1'); }, Error); 153 | assert.throws(function () { parser.parse('0b1.1'); }, Error); 154 | assert.throws(function () { parser.parse('0b2'); }, Error); 155 | assert.throws(function () { parser.parse('0bb0'); }, Error); 156 | assert.throws(function () { parser.parse('0b1e+1'); }, Error); 157 | assert.throws(function () { parser.parse('1b0'); }, Error); 158 | }); 159 | 160 | it('should fail on unknown characters', function () { 161 | assert.throws(function () { parser.parse('1 + @'); }, Error); 162 | }); 163 | 164 | it('should fail with partial operators', function () { 165 | assert.throws(function () { parser.parse('"a" | "b"'); }, Error); 166 | assert.throws(function () { parser.parse('2 = 2'); }, Error); 167 | assert.throws(function () { parser.parse('2 ! 3'); }, Error); 168 | assert.throws(function () { parser.parse('1 o 0'); }, Error); 169 | assert.throws(function () { parser.parse('1 an 2'); }, Error); 170 | assert.throws(function () { parser.parse('1 a 2'); }, Error); 171 | }); 172 | 173 | it('should parse strings', function () { 174 | assert.strictEqual(parser.evaluate('\'asdf\''), 'asdf'); 175 | assert.strictEqual(parser.evaluate('"asdf"'), 'asdf'); 176 | assert.strictEqual(parser.evaluate('""'), ''); 177 | assert.strictEqual(parser.evaluate('\'\''), ''); 178 | assert.strictEqual(parser.evaluate('" "'), ' '); 179 | assert.strictEqual(parser.evaluate('"a\nb\tc"'), 'a\nb\tc'); 180 | assert.strictEqual(parser.evaluate('"Nested \'single quotes\'"'), 'Nested \'single quotes\''); 181 | assert.strictEqual(parser.evaluate('\'Nested "double quotes"\''), 'Nested "double quotes"'); 182 | assert.strictEqual(parser.evaluate('\'Single quotes \\\'inside\\\' single quotes\''), 'Single quotes \'inside\' single quotes'); 183 | assert.strictEqual(parser.evaluate('"Double quotes \\"inside\\" double quotes"'), 'Double quotes "inside" double quotes'); 184 | assert.strictEqual(parser.evaluate('"\n"'), '\n'); 185 | assert.strictEqual(parser.evaluate('"\\\'\\"\\\\\\/\\b\\f\\n\\r\\t\\u1234"'), '\'"\\/\b\f\n\r\t\u1234'); 186 | assert.strictEqual(parser.evaluate('"\'\\"\\\\\\/\\b\\f\\n\\r\\t\\u1234"'), '\'"\\/\b\f\n\r\t\u1234'); 187 | assert.strictEqual(parser.evaluate('\'\\\'\\"\\\\\\/\\b\\f\\n\\r\\t\\u1234\''), '\'"\\/\b\f\n\r\t\u1234'); 188 | assert.strictEqual(parser.evaluate('\'\\\'"\\\\\\/\\b\\f\\n\\r\\t\\u1234\''), '\'"\\/\b\f\n\r\t\u1234'); 189 | assert.strictEqual(parser.evaluate('"\\uFFFF"'), '\uFFFF'); 190 | assert.strictEqual(parser.evaluate('"\\u0123"'), '\u0123'); 191 | assert.strictEqual(parser.evaluate('"\\u4567"'), '\u4567'); 192 | assert.strictEqual(parser.evaluate('"\\u89ab"'), '\u89ab'); 193 | assert.strictEqual(parser.evaluate('"\\ucdef"'), '\ucdef'); 194 | assert.strictEqual(parser.evaluate('"\\uABCD"'), '\uABCD'); 195 | assert.strictEqual(parser.evaluate('"\\uEF01"'), '\uEF01'); 196 | assert.strictEqual(parser.evaluate('"\\u11111"'), '\u11111'); 197 | }); 198 | 199 | it('should fail on bad strings', function () { 200 | assert.throws(function () { parser.parse('\'asdf"'); }, Error); 201 | assert.throws(function () { parser.parse('"asdf\''); }, Error); 202 | assert.throws(function () { parser.parse('"asdf'); }, Error); 203 | assert.throws(function () { parser.parse('\'asdf'); }, Error); 204 | assert.throws(function () { parser.parse('\'asdf\\'); }, Error); 205 | assert.throws(function () { parser.parse('\''); }, Error); 206 | assert.throws(function () { parser.parse('"'); }, Error); 207 | assert.throws(function () { parser.parse('"\\x"'); }, Error); 208 | assert.throws(function () { parser.parse('"\\u123"'); }, Error); 209 | assert.throws(function () { parser.parse('"\\u12"'); }, Error); 210 | assert.throws(function () { parser.parse('"\\u1"'); }, Error); 211 | assert.throws(function () { parser.parse('"\\uGGGG"'); }, Error); 212 | }); 213 | 214 | it('should parse arrays correctly', function () { 215 | assert.strictEqual(parser.parse('[1, 2, 3+4, 5*6, (7/8)]').toString(), '[1, 2, (3 + 4), (5 * 6), (7 / 8)]'); 216 | }); 217 | 218 | it('should parse empty arrays correctly', function () { 219 | assert.strictEqual(parser.parse('[]').toString(), '[]'); 220 | }); 221 | 222 | it('should fail with missing ]', function () { 223 | assert.throws(function () { parser.parse('[1, 2, 3+4, 5*6, (7/8)'); }, Error); 224 | }); 225 | 226 | it('should parse operators that look like functions as function calls', function () { 227 | assert.strictEqual(parser.parse('sin 2^3').toString(), '(sin (2 ^ 3))'); 228 | assert.strictEqual(parser.parse('sin(2)^3').toString(), '((sin 2) ^ 3)'); 229 | assert.strictEqual(parser.parse('sin 2^3').evaluate(), Math.sin(Math.pow(2, 3))); 230 | assert.strictEqual(parser.parse('sin(2)^3').evaluate(), Math.pow(Math.sin(2), 3)); 231 | }); 232 | 233 | it('should parse named prefix operators as function names at the end of expressions', function () { 234 | assert.strictEqual(parser.parse('sin;').toString(), '(sin)'); 235 | assert.strictEqual(parser.parse('(sin)').toString(), 'sin'); 236 | assert.strictEqual(parser.parse('sin; (2)^3').toString(), '(sin;(2 ^ 3))'); 237 | assert.deepStrictEqual(parser.parse('f(sin, sqrt)').evaluate({ f: function (a, b) { return [ a, b ]; }}), [ Math.sin, Math.sqrt ]); 238 | assert.strictEqual(parser.parse('sin').evaluate(), Math.sin); 239 | assert.strictEqual(parser.parse('cos;').evaluate(), Math.cos); 240 | assert.strictEqual(parser.parse('cos;tan').evaluate(), Math.tan); 241 | assert.strictEqual(parser.parse('(floor)').evaluate(), Math.floor); 242 | assert.strictEqual(parser.parse('4; ceil').evaluate(), Math.ceil); 243 | }); 244 | 245 | it('unary + and - should not be parsed as function calls', function () { 246 | assert.strictEqual(parser.parse('-2^3').toString(), '(-(2 ^ 3))'); 247 | assert.strictEqual(parser.parse('-(2)^3').toString(), '(-(2 ^ 3))'); 248 | }); 249 | 250 | it('should treat ∙ and • as * operators', function () { 251 | assert.strictEqual(parser.parse('2 ∙ 3').toString(), '(2 * 3)'); 252 | assert.strictEqual(parser.parse('4 • 5').toString(), '(4 * 5)'); 253 | }); 254 | 255 | it('should parse variables that start with operators', function () { 256 | assert.strictEqual(parser.parse('org > 5').toString(), '(org > 5)'); 257 | assert.strictEqual(parser.parse('android * 2').toString(), '(android * 2)'); 258 | assert.strictEqual(parser.parse('single == 1').toString(), '(single == 1)'); 259 | }); 260 | 261 | it('should parse valid variable names correctly', function () { 262 | assert.deepStrictEqual(parser.parse('a').variables(), [ 'a' ]); 263 | assert.deepStrictEqual(parser.parse('abc').variables(), [ 'abc' ]); 264 | assert.deepStrictEqual(parser.parse('a+b').variables(), [ 'a', 'b' ]); 265 | assert.deepStrictEqual(parser.parse('ab+c').variables(), [ 'ab', 'c' ]); 266 | assert.deepStrictEqual(parser.parse('a1').variables(), [ 'a1' ]); 267 | assert.deepStrictEqual(parser.parse('a_1').variables(), [ 'a_1' ]); 268 | assert.deepStrictEqual(parser.parse('a_').variables(), [ 'a_' ]); 269 | assert.deepStrictEqual(parser.parse('a_c').variables(), [ 'a_c' ]); 270 | assert.deepStrictEqual(parser.parse('A').variables(), [ 'A' ]); 271 | assert.deepStrictEqual(parser.parse('ABC').variables(), [ 'ABC' ]); 272 | assert.deepStrictEqual(parser.parse('A+B').variables(), [ 'A', 'B' ]); 273 | assert.deepStrictEqual(parser.parse('AB+C').variables(), [ 'AB', 'C' ]); 274 | assert.deepStrictEqual(parser.parse('A1').variables(), [ 'A1' ]); 275 | assert.deepStrictEqual(parser.parse('A_1').variables(), [ 'A_1' ]); 276 | assert.deepStrictEqual(parser.parse('A_C').variables(), [ 'A_C' ]); 277 | assert.deepStrictEqual(parser.parse('abcdefg/hijklmnop+qrstuvwxyz').variables(), [ 'abcdefg', 'hijklmnop', 'qrstuvwxyz' ]); 278 | assert.deepStrictEqual(parser.parse('ABCDEFG/HIJKLMNOP+QRSTUVWXYZ').variables(), [ 'ABCDEFG', 'HIJKLMNOP', 'QRSTUVWXYZ' ]); 279 | assert.deepStrictEqual(parser.parse('abc123+def456*ghi789/jkl0').variables(), [ 'abc123', 'def456', 'ghi789', 'jkl0' ]); 280 | assert.deepStrictEqual(parser.parse('_').variables(), [ '_' ]); 281 | assert.deepStrictEqual(parser.parse('_x').variables(), [ '_x' ]); 282 | assert.deepStrictEqual(parser.parse('$x').variables(), [ '$x' ]); 283 | assert.deepStrictEqual(parser.parse('$xyz').variables(), [ '$xyz' ]); 284 | assert.deepStrictEqual(parser.parse('$a_sdf').variables(), [ '$a_sdf' ]); 285 | assert.deepStrictEqual(parser.parse('$xyz_123').variables(), [ '$xyz_123' ]); 286 | assert.deepStrictEqual(parser.parse('_xyz_123').variables(), [ '_xyz_123' ]); 287 | }); 288 | 289 | it('should not parse invalid variables', function () { 290 | assert.throws(function () { parser.parse('a$x'); }, /parse error/); 291 | assert.throws(function () { parser.parse('ab$'); }, /parse error/); 292 | }); 293 | 294 | it('should not parse a single $ as a variable', function () { 295 | assert.throws(function () { parser.parse('$'); }, /parse error/); 296 | }); 297 | 298 | it('should not allow leading digits in variable names', function () { 299 | assert.throws(function () { parser.parse('1a'); }, /parse error/); 300 | assert.throws(function () { parser.parse('1_'); }, /parse error/); 301 | assert.throws(function () { parser.parse('1_a'); }, /parse error/); 302 | }); 303 | 304 | it('should not allow leading digits or _ after $ in variable names', function () { 305 | assert.throws(function () { parser.parse('$0'); }, /parse error/); 306 | assert.throws(function () { parser.parse('$1a'); }, /parse error/); 307 | assert.throws(function () { parser.parse('$_'); }, /parse error/); 308 | assert.throws(function () { parser.parse('$_x'); }, /parse error/); 309 | }); 310 | 311 | it('should track token positions correctly', function () { 312 | assert.throws(function () { parser.parse('@23'); }, /parse error \[1:1]/); 313 | assert.throws(function () { parser.parse('\n@23'); }, /parse error \[2:1]/); 314 | assert.throws(function () { parser.parse('1@3'); }, /parse error \[1:2]/); 315 | assert.throws(function () { parser.parse('12@'); }, /parse error \[1:3]/); 316 | assert.throws(function () { parser.parse('12@\n'); }, /parse error \[1:3]/); 317 | assert.throws(function () { parser.parse('@23 +\n45 +\n6789'); }, /parse error \[1:1]/); 318 | assert.throws(function () { parser.parse('1@3 +\n45 +\n6789'); }, /parse error \[1:2]/); 319 | assert.throws(function () { parser.parse('12@ +\n45 +\n6789'); }, /parse error \[1:3]/); 320 | assert.throws(function () { parser.parse('123 +\n@5 +\n6789'); }, /parse error \[2:1]/); 321 | assert.throws(function () { parser.parse('123 +\n4@ +\n6789'); }, /parse error \[2:2]/); 322 | assert.throws(function () { parser.parse('123 +\n45@+\n6789'); }, /parse error \[2:3]/); 323 | assert.throws(function () { parser.parse('123 +\n45 +\n@789'); }, /parse error \[3:1]/); 324 | assert.throws(function () { parser.parse('123 +\n45 +\n6@89'); }, /parse error \[3:2]/); 325 | assert.throws(function () { parser.parse('123 +\n45 +\n67@9'); }, /parse error \[3:3]/); 326 | assert.throws(function () { parser.parse('123 +\n45 +\n679@'); }, /parse error \[3:4]/); 327 | assert.throws(function () { parser.parse('123 +\n\n679@'); }, /parse error \[3:4]/); 328 | assert.throws(function () { parser.parse('123 +\n\n\n\n\n679@'); }, /parse error \[6:4]/); 329 | }); 330 | 331 | it('should allow operators to be disabled', function () { 332 | var parser = new Parser({ 333 | operators: { 334 | add: false, 335 | sin: false, 336 | remainder: false, 337 | divide: false 338 | } 339 | }); 340 | assert.throws(function () { parser.parse('+1'); }, /\+/); 341 | assert.throws(function () { parser.parse('1 + 2'); }, /\+/); 342 | assert.strictEqual(parser.parse('sin(0)').toString(), 'sin(0)'); 343 | assert.throws(function () { parser.evaluate('sin(0)'); }, /sin/); 344 | assert.throws(function () { parser.parse('4 % 5'); }, /%/); 345 | assert.throws(function () { parser.parse('4 / 5'); }, /\//); 346 | }); 347 | 348 | it('should allow operators to be explicitly enabled', function () { 349 | var parser = new Parser({ 350 | operators: { 351 | add: true, 352 | sqrt: true, 353 | divide: true, 354 | 'in': true, 355 | assignment: true 356 | } 357 | }); 358 | assert.strictEqual(parser.evaluate('+(-1)'), -1); 359 | assert.strictEqual(parser.evaluate('sqrt(16)'), 4); 360 | assert.strictEqual(parser.evaluate('4 / 6'), 2 / 3); 361 | assert.strictEqual(parser.evaluate('3 in array', { array: [ 1, 2, 3 ] }), true); 362 | assert.strictEqual(parser.evaluate('x = 4', { x: 2 }), 4); 363 | }); 364 | }); 365 | 366 | it('should allow addition operator to be disabled', function () { 367 | var parser = new Parser({ 368 | operators: { 369 | add: false 370 | } 371 | }); 372 | 373 | assert.throws(function () { parser.parse('2 + 3'); }, /\+/); 374 | }); 375 | 376 | it('should allow comparison operators to be disabled', function () { 377 | var parser = new Parser({ 378 | operators: { 379 | comparison: false 380 | } 381 | }); 382 | 383 | assert.throws(function () { parser.parse('1 == 1'); }, /=/); 384 | assert.throws(function () { parser.parse('1 != 2'); }, /!/); 385 | assert.throws(function () { parser.parse('1 > 0'); }, />/); 386 | assert.throws(function () { parser.parse('1 >= 0'); }, />/); 387 | assert.throws(function () { parser.parse('1 < 2'); }, / 1 ? x*f(x-1) : 1)(5)', function () { 163 | var parser = new Parser(); 164 | assert.strictEqual(parser.evaluate('(f(x) = x > 1 ? x*f(x-1) : 1)(5)'), 120); 165 | assert.strictEqual(parser.evaluate('(f(x) = x > 1 ? x*f(x-1) : 1); f(6)'), 720); 166 | }); 167 | 168 | it('f(x) = x > 1 ? x*f(x-1) : 1; f(6); f(5)', function () { 169 | var parser = new Parser(); 170 | assert.strictEqual(parser.evaluate('f(x) = x > 1 ? x*f(x-1) : 1; f(6)'), 720); 171 | assert.strictEqual(parser.evaluate('f(x) = x > 1 ? x*f(x-1) : 1; f(6); f(5)'), 120); 172 | }); 173 | 174 | it('f(x) = x > 1 ? x*f(x-1) : 1', function () { 175 | var parser = new Parser(); 176 | var obj = { f: null }; 177 | assert.strictEqual(parser.evaluate('f(x) = x > 1 ? x*f(x-1) : 1', obj) instanceof Function, true); 178 | assert.strictEqual(obj.f instanceof Function, true); 179 | assert.strictEqual(obj.f(6), 720); 180 | assert.strictEqual(obj.f(5), 120); 181 | assert.strictEqual(obj.f(4), 24); 182 | assert.strictEqual(obj.f(3), 6); 183 | }); 184 | 185 | it('3 ; 2 ; 1', function () { 186 | assert.strictEqual(Parser.evaluate('3 ; 2 ; 1'), 1); 187 | }); 188 | 189 | it('3 ; 2 ; 1 ;', function () { 190 | assert.strictEqual(Parser.evaluate('3 ; 2 ; 1 ;'), 1); 191 | }); 192 | 193 | it('x = 3 ; y = 4 ; z = x * y', function () { 194 | var parser = new Parser(); 195 | assert.strictEqual(parser.evaluate('x = 3 ; y = 4 ; z = x * y'), 12); 196 | }); 197 | 198 | it('x=3;y=4;z=x*y;', function () { 199 | var parser = new Parser(); 200 | assert.strictEqual(parser.evaluate('x=3;y=4;z=x*y;'), 12); 201 | }); 202 | 203 | it('1 + (( 3 ; 4 ) + 5)', function () { 204 | var parser = new Parser(); 205 | assert.strictEqual(parser.evaluate('1 + (( 3 ; 4 ) + 5)'), 10); 206 | }); 207 | 208 | it('2+(x=3;y=4;z=x*y)+5', function () { 209 | var parser = new Parser(); 210 | assert.strictEqual(parser.evaluate('2+(x=3;y=4;z=x*y)+5'), 19); 211 | }); 212 | 213 | it('[1, 2, 3]', function () { 214 | assert.deepStrictEqual(Parser.evaluate('[1, 2, 3]'), [1, 2, 3]); 215 | }); 216 | 217 | it('[1, 2, 3, [4, [5, 6]]]', function () { 218 | assert.deepStrictEqual(Parser.evaluate('[1, 2, 3, [4, [5, 6]]]'), [1, 2, 3, [4, [5, 6]]]); 219 | }); 220 | 221 | it('["a", ["b", ["c"]], true, 1 + 2 + 3]', function () { 222 | assert.deepStrictEqual(Parser.evaluate('["a", ["b", ["c"]], true, 1 + 2 + 3]'), ['a', ['b', ['c']], true, 6]); 223 | }); 224 | 225 | it('should fail trying to call a non-function', function () { 226 | assert.throws(function () { Parser.evaluate('f()', { f: 2 }); }, Error); 227 | }); 228 | 229 | it('$x * $y_+$a1*$z - $b2', function () { 230 | assert.strictEqual(Parser.evaluate('$x * $y_+$a1*$z - $b2', { $a1: 3, $b2: 5, $x: 7, $y_: 9, $z: 11 }), 91); 231 | }); 232 | 233 | it('max(conf.limits.lower, conf.limits.upper)', function () { 234 | assert.strictEqual(Parser.evaluate('max(conf.limits.lower, conf.limits.upper)', { conf: { limits: { lower: 4, upper: 9 } } }), 9); 235 | }); 236 | 237 | it('fn.max(conf.limits.lower, conf.limits.upper)', function () { 238 | assert.strictEqual(Parser.evaluate('fn.max(conf.limits.lower, conf.limits.upper)', { fn: { max: Math.max }, conf: { limits: { lower: 4, upper: 9 } } }), 9); 239 | }); 240 | 241 | it('[1, 2+3, 4*5, 6/7, [8, 9, 10], "1" || "1"]', function () { 242 | assert.strictEqual(JSON.stringify(Parser.evaluate('[1, 2+3, 4*5, 6/7, [8, 9, 10], "1" || "1"]')), JSON.stringify([1, 5, 20, 6 / 7, [8, 9, 10], '11'])); 243 | }); 244 | 245 | it('1 ? 1 : 0', function () { 246 | assert.strictEqual(Parser.evaluate('1 ? 1 : 0'), 1); 247 | }); 248 | 249 | it('0 ? 1 : 0', function () { 250 | assert.strictEqual(Parser.evaluate('0 ? 1 : 0'), 0); 251 | }); 252 | 253 | it('1==1 or 2==1 ? 39 : 0', function () { 254 | assert.strictEqual(Parser.evaluate('1==1 or 2==1 ? 39 : 0'), 39); 255 | }); 256 | 257 | it('1==1 or 1==2 ? -4 + 8 : 0', function () { 258 | assert.strictEqual(Parser.evaluate('1==1 or 1==2 ? -4 + 8 : 0'), 4); 259 | }); 260 | 261 | it('3 and 6 ? 45 > 5 * 11 ? 3 * 3 : 2.4 : 0', function () { 262 | assert.strictEqual(Parser.evaluate('3 and 6 ? 45 > 5 * 11 ? 3 * 3 : 2.4 : 0'), 2.4); 263 | }); 264 | }); 265 | 266 | describe('substitute()', function () { 267 | var parser = new Parser(); 268 | 269 | var expr = parser.parse('2 * x + 1'); 270 | var expr2 = expr.substitute('x', '4 * x'); 271 | it('((2*(4*x))+1)', function () { 272 | assert.strictEqual(expr2.evaluate({ x: 3 }), 25); 273 | }); 274 | 275 | var expr3 = expr.substitute('x', '4 * x.y.z'); 276 | it('((2*(4*x.y.z))+1)', function () { 277 | assert.strictEqual(expr3.evaluate({ x: { y: { z: 3 } } }), 25); 278 | }); 279 | 280 | var expr4 = parser.parse('-x').substitute('x', '-4 + y'); 281 | it('-(-4 + y)', function () { 282 | assert.strictEqual(expr4.toString(), '(-((-4) + y))'); 283 | assert.strictEqual(expr4.evaluate({ y: 2 }), 2); 284 | }); 285 | 286 | var expr5 = parser.parse('x + y').substitute('y', 'x ? 1 : 2'); 287 | it('x + (x ? 1 : 2)', function () { 288 | assert.strictEqual(expr5.toString(), '(x + (x ? (1) : (2)))'); 289 | assert.strictEqual(expr5.evaluate({ x: 3 }), 4); 290 | assert.strictEqual(expr5.evaluate({ x: 0 }), 2); 291 | }); 292 | 293 | var expr6 = parser.parse('x ? y : z').substitute('y', 'x'); 294 | it('x ? x : z', function () { 295 | assert.strictEqual(expr6.toString(), '(x ? (x) : (z))'); 296 | assert.strictEqual(expr6.evaluate({ x: 1, z: 2 }), 1); 297 | assert.strictEqual(expr6.evaluate({ x: 0, z: 2 }), 2); 298 | }); 299 | 300 | var expr7 = expr.substitute('x', parser.parse('4 * x')); 301 | it('should substitute expressions', function () { 302 | assert.strictEqual(expr7.toString(), '((2 * (4 * x)) + 1)'); 303 | assert.strictEqual(expr7.evaluate({ x: 3 }), 25); 304 | }); 305 | 306 | var expr8 = parser.parse('x = x + 1').substitute('x', '7'); 307 | it('should not replace assigned variables', function () { 308 | assert.strictEqual(expr8.toString(), '(x = ((7 + 1)))'); 309 | var vars = { x: 42 }; 310 | assert.strictEqual(expr8.evaluate(vars), 8); 311 | assert.strictEqual(vars.x, 8); 312 | }); 313 | }); 314 | 315 | describe('simplify()', function () { 316 | var expr = Parser.parse('x * (y * atan(1))').simplify({ y: 4 }); 317 | it('(x*3.141592653589793)', function () { 318 | assert.strictEqual(expr.toString(), '(x * 3.141592653589793)'); 319 | }); 320 | 321 | it('6.283185307179586', function () { 322 | assert.strictEqual(expr.simplify({ x: 2 }).toString(), '6.283185307179586'); 323 | }); 324 | 325 | it('(x/2) ? y : z', function () { 326 | assert.strictEqual(Parser.parse('(x/2) ? y : z').simplify({ x: 4 }).toString(), '(2 ? (y) : (z))'); 327 | }); 328 | 329 | it('x ? (y + 1) : z', function () { 330 | assert.strictEqual(Parser.parse('x ? (y + 1) : z').simplify({ y: 2 }).toString(), '(x ? (3) : (z))'); 331 | }); 332 | 333 | it('x ? y : (z * 4)', function () { 334 | assert.strictEqual(Parser.parse('x ? y : (z * 4)').simplify({ z: 3 }).toString(), '(x ? (y) : (12))'); 335 | }); 336 | 337 | it('x = 2*x', function () { 338 | assert.strictEqual(new Parser().parse('x = 2*x').simplify({ x: 3 }).toString(), '(x = (6))'); 339 | }); 340 | 341 | it('(f(x) = x * y)(3)', function () { 342 | assert.strictEqual(new Parser().parse('(f(x) = x * y)(3)').simplify({ y: 5 }).toString(), '(f(x) = ((x * 5)))(3)'); 343 | }); 344 | 345 | it('a[2] + b[3]', function () { 346 | assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [ 0, 0, 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '9'); 347 | assert.strictEqual(Parser.parse('a[2] + b[3]').simplify({ a: [ 0, 0, 5, 0 ] }).toString(), '(5 + b[3])'); 348 | assert.strictEqual(Parser.parse('a[2] + b[5 - 2]').simplify({ b: [ 0, 0, 0, 4, 0 ] }).toString(), '(a[2] + 4)'); 349 | assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [ 0, 0, 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '([0, 0, 5, 0][two] + 4)'); 350 | assert.strictEqual(Parser.parse('a[two] + b[3]').simplify({ a: [ 0, 'New\nLine', 5, 0 ], b: [ 0, 0, 0, 4, 0 ] }).toString(), '([0, "New\\nLine", 5, 0][two] + 4)'); 351 | }); 352 | }); 353 | 354 | describe('variables()', function () { 355 | var expr = Parser.parse('x * (y * atan2(1, 2)) + z.y.x'); 356 | it('["x", "y", "z.y.x"]', function () { 357 | assert.deepStrictEqual(expr.variables(), ['x', 'y', 'z']); 358 | }); 359 | 360 | it('["x", "z.y.x"]', function () { 361 | assert.deepStrictEqual(expr.simplify({ y: 4 }).variables(), ['x', 'z']); 362 | }); 363 | 364 | it('["x"]', function () { 365 | assert.deepStrictEqual(expr.simplify({ y: 4, z: { y: { x: 5 } } }).variables(), ['x']); 366 | }); 367 | 368 | it('a or b ? c + d : e * f', function () { 369 | assert.deepStrictEqual(Parser.parse('a or b ? c + d : e * f').variables(), ['a', 'b', 'c', 'd', 'e', 'f']); 370 | }); 371 | 372 | it('$x * $y_+$a1*$z - $b2', function () { 373 | assert.deepStrictEqual(Parser.parse('$x * $y_+$a1*$z - $b2').variables(), ['$x', '$y_', '$a1', '$z', '$b2']); 374 | }); 375 | 376 | it('user.age + 2', function () { 377 | assert.deepStrictEqual(Parser.parse('user.age + 2').variables(), ['user']); 378 | }); 379 | 380 | it('user.age + 2 with { withMembers: false } option', function () { 381 | assert.deepStrictEqual(Parser.parse('user.age + 2').variables({ withMembers: false }), ['user']); 382 | }); 383 | 384 | it('user.age + 2 with { withMembers: true } option', function () { 385 | var expr = Parser.parse('user.age + 2'); 386 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['user.age']); 387 | }); 388 | 389 | it('x.y ? x.y.z : default.z with { withMembers: true } option', function () { 390 | var expr = Parser.parse('x.y ? x.y.z : default.z'); 391 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['x.y.z', 'default.z', 'x.y']); 392 | }); 393 | 394 | it('x + x.y + x.z with { withMembers: true } option', function () { 395 | var expr = Parser.parse('x + x.y + x.z'); 396 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['x', 'x.y', 'x.z']); 397 | }); 398 | 399 | it('x.y < 3 ? 2 * x.y.z : default.z + 1 with { withMembers: true } option', function () { 400 | var expr = Parser.parse('x.y < 3 ? 2 * x.y.z : default.z + 1'); 401 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['x.y', 'x.y.z', 'default.z']); 402 | }); 403 | 404 | it('user.age with { withMembers: true } option', function () { 405 | var expr = Parser.parse('user.age'); 406 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['user.age']); 407 | }); 408 | 409 | it('x with { withMembers: true } option', function () { 410 | var expr = Parser.parse('x'); 411 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['x']); 412 | }); 413 | 414 | it('x with { withMembers: false } option', function () { 415 | var expr = Parser.parse('x'); 416 | assert.deepStrictEqual(expr.variables({ withMembers: false }), ['x']); 417 | }); 418 | 419 | it('max(conf.limits.lower, conf.limits.upper) with { withMembers: false } option', function () { 420 | var expr = Parser.parse('max(conf.limits.lower, conf.limits.upper)'); 421 | assert.deepStrictEqual(expr.variables({ withMembers: false }), ['conf']); 422 | }); 423 | 424 | it('max(conf.limits.lower, conf.limits.upper) with { withMembers: true } option', function () { 425 | var expr = Parser.parse('max(conf.limits.lower, conf.limits.upper)'); 426 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['conf.limits.lower', 'conf.limits.upper']); 427 | }); 428 | 429 | it('fn.max(conf.limits.lower, conf.limits.upper) with { withMembers: false } option', function () { 430 | var expr = Parser.parse('fn.max(conf.limits.lower, conf.limits.upper)'); 431 | assert.deepStrictEqual(expr.variables({ withMembers: false }), ['fn', 'conf']); 432 | }); 433 | 434 | it('fn.max(conf.limits.lower, conf.limits.upper) with { withMembers: true } option', function () { 435 | var expr = Parser.parse('fn.max(conf.limits.lower, conf.limits.upper)'); 436 | assert.deepStrictEqual(expr.variables({ withMembers: true }), ['fn.max', 'conf.limits.lower', 'conf.limits.upper']); 437 | }); 438 | 439 | it('x = y + z', function () { 440 | assert.deepStrictEqual(new Parser().parse('x = y + z').variables(), ['x', 'y', 'z']); 441 | }); 442 | 443 | it('f(x, y, z) = x + y + z', function () { 444 | var parser = new Parser(); 445 | assert.deepStrictEqual(parser.parse('f(x, y, z) = x + y + z').variables(), ['f', 'x', 'y', 'z']); 446 | }); 447 | }); 448 | 449 | describe('symbols()', function () { 450 | var expr = Parser.parse('x * (y * atan2(1, 2)) + z.y.x'); 451 | it('["x", "y", "z.y.x"]', function () { 452 | assert.deepStrictEqual(expr.symbols(), ['x', 'y', 'atan2', 'z']); 453 | }); 454 | 455 | it('["x", "z.y.x"]', function () { 456 | assert.deepStrictEqual(expr.simplify({ y: 4 }).symbols(), ['x', 'atan2', 'z']); 457 | }); 458 | 459 | it('["x"]', function () { 460 | assert.deepStrictEqual(expr.simplify({ y: 4, z: { y: { x: 5 } } }).symbols(), ['x', 'atan2']); 461 | }); 462 | 463 | it('a or b ? c + d : e * f', function () { 464 | assert.deepStrictEqual(Parser.parse('a or b ? c + d : e * f').symbols(), ['a', 'b', 'c', 'd', 'e', 'f']); 465 | }); 466 | 467 | it('user.age + 2', function () { 468 | assert.deepStrictEqual(Parser.parse('user.age + 2').symbols(), ['user']); 469 | }); 470 | 471 | it('user.age + 2 with { withMembers: false } option', function () { 472 | assert.deepStrictEqual(Parser.parse('user.age + 2').symbols({ withMembers: false }), ['user']); 473 | }); 474 | 475 | it('user.age + 2 with { withMembers: true } option', function () { 476 | var expr = Parser.parse('user.age + 2'); 477 | assert.deepStrictEqual(expr.symbols({ withMembers: true }), ['user.age']); 478 | }); 479 | 480 | it('x.y ? x.y.z : default.z with { withMembers: true } option', function () { 481 | var expr = Parser.parse('x.y ? x.y.z : default.z'); 482 | assert.deepStrictEqual(expr.symbols({ withMembers: true }), ['x.y.z', 'default.z', 'x.y']); 483 | }); 484 | 485 | it('x.y < 3 ? 2 * x.y.z : default.z + 1 with { withMembers: true } option', function () { 486 | var expr = Parser.parse('x.y < 3 ? 2 * x.y.z : default.z + 1'); 487 | assert.deepStrictEqual(expr.symbols({ withMembers: true }), ['x.y', 'x.y.z', 'default.z']); 488 | }); 489 | 490 | it('user.age with { withMembers: true } option', function () { 491 | var expr = Parser.parse('user.age'); 492 | assert.deepStrictEqual(expr.symbols({ withMembers: true }), ['user.age']); 493 | }); 494 | 495 | it('x with { withMembers: true } option', function () { 496 | var expr = Parser.parse('x'); 497 | assert.deepStrictEqual(expr.symbols({ withMembers: true }), ['x']); 498 | }); 499 | 500 | it('x with { withMembers: false } option', function () { 501 | var expr = Parser.parse('x'); 502 | assert.deepStrictEqual(expr.symbols({ withMembers: false }), ['x']); 503 | }); 504 | 505 | it('x = y + z', function () { 506 | assert.deepStrictEqual(new Parser().parse('x = y + z').symbols(), ['x', 'y', 'z']); 507 | }); 508 | }); 509 | 510 | describe('toString()', function () { 511 | var parser = new Parser(); 512 | 513 | it('2 ^ x', function () { 514 | assert.strictEqual(parser.parse('2 ^ x').toString(), '(2 ^ x)'); 515 | }); 516 | 517 | it('2 * x + 1', function () { 518 | assert.strictEqual(parser.parse('2 * x + 1').toString(), '((2 * x) + 1)'); 519 | }); 520 | 521 | it('2 + 3 * x', function () { 522 | assert.strictEqual(parser.parse('2 + 3 * x').toString(), '(2 + (3 * x))'); 523 | }); 524 | 525 | it('(2 + 3) * x', function () { 526 | assert.strictEqual(parser.parse('(2 + 3) * x').toString(), '((2 + 3) * x)'); 527 | }); 528 | 529 | it('2-3^x', function () { 530 | assert.strictEqual(parser.parse('2-3^x').toString(), '(2 - (3 ^ x))'); 531 | }); 532 | 533 | it('-2-3^x', function () { 534 | assert.strictEqual(parser.parse('-2-3^x').toString(), '((-2) - (3 ^ x))'); 535 | }); 536 | 537 | it('-3^x', function () { 538 | assert.strictEqual(parser.parse('-3^x').toString(), '(-(3 ^ x))'); 539 | }); 540 | 541 | it('(-3)^x', function () { 542 | assert.strictEqual(parser.parse('(-3)^x').toString(), '((-3) ^ x)'); 543 | }); 544 | 545 | it('2 ^ x.y', function () { 546 | assert.strictEqual(parser.parse('2^x.y').toString(), '(2 ^ x.y)'); 547 | }); 548 | 549 | it('2 + 3 * foo.bar.baz', function () { 550 | assert.strictEqual(parser.parse('2 + 3 * foo.bar.baz').toString(), '(2 + (3 * foo.bar.baz))'); 551 | }); 552 | 553 | it('sqrt 10/-1', function () { 554 | assert.strictEqual(parser.parse('sqrt 10/-1').toString(), '((sqrt 10) / (-1))'); 555 | }); 556 | 557 | it('10*-1', function () { 558 | assert.strictEqual(parser.parse('10*-1').toString(), '(10 * (-1))'); 559 | }); 560 | 561 | it('10+-1', function () { 562 | assert.strictEqual(parser.parse('10+-1').toString(), '(10 + (-1))'); 563 | }); 564 | 565 | it('10+ +1', function () { 566 | assert.strictEqual(parser.parse('10+ +1').toString(), '(10 + (+1))'); 567 | }); 568 | 569 | it('sin 2^-4', function () { 570 | assert.strictEqual(parser.parse('sin 2^-4').toString(), '(sin (2 ^ (-4)))'); 571 | }); 572 | 573 | it('a ? b : c', function () { 574 | assert.strictEqual(parser.parse('a ? b : c').toString(), '(a ? (b) : (c))'); 575 | }); 576 | 577 | it('a ? b : c ? d : e', function () { 578 | assert.strictEqual(parser.parse('a ? b : c ? d : e').toString(), '(a ? (b) : ((c ? (d) : (e))))'); 579 | }); 580 | 581 | it('a ? b ? c : d : e', function () { 582 | assert.strictEqual(parser.parse('a ? b ? c : d : e').toString(), '(a ? ((b ? (c) : (d))) : (e))'); 583 | }); 584 | 585 | it('a == 2 ? b + 1 : c * 2', function () { 586 | assert.strictEqual(parser.parse('a == 2 ? b + 1 : c * 2').toString(), '((a == 2) ? ((b + 1)) : ((c * 2)))'); 587 | }); 588 | 589 | it('floor(random() * 10)', function () { 590 | assert.strictEqual(parser.parse('floor(random() * 10)').toString(), '(floor (random() * 10))'); 591 | }); 592 | 593 | it('hypot(random(), max(2, x, y))', function () { 594 | assert.strictEqual(parser.parse('hypot(random(), max(2, x, y))').toString(), 'hypot(random(), max(2, x, y))'); 595 | }); 596 | 597 | it('not 0 or 1 and 2', function () { 598 | assert.strictEqual(parser.parse('not 0 or 1 and 2').toString(), '((not 0) or ((1 and (2))))'); 599 | }); 600 | 601 | it('a < b or c > d and e <= f or g >= h and i == j or k != l', function () { 602 | assert.strictEqual(parser.parse('a < b or c > d and e <= f or g >= h and i == j or k != l').toString(), 603 | '((((a < b) or (((c > d) and ((e <= f))))) or (((g >= h) and ((i == j))))) or ((k != l)))'); 604 | }); 605 | 606 | it('x = x + 1', function () { 607 | assert.strictEqual(parser.parse('x = x + 1').toString(), '(x = ((x + 1)))'); 608 | }); 609 | 610 | it('x = y = x + 1', function () { 611 | assert.strictEqual(parser.parse('x = y = x + 1').toString(), '(x = ((y = ((x + 1)))))'); 612 | }); 613 | 614 | it('3 ; 2 ; 1', function () { 615 | assert.strictEqual(parser.parse('3 ; 2 ; 1').toString(), '(3;(2;1))'); 616 | }); 617 | 618 | it('3 ; 2 ; 1 ;', function () { 619 | assert.strictEqual(parser.parse('3 ; 2 ; 1 ;').toString(), '(3;(2;(1)))'); 620 | }); 621 | 622 | it('x = 3 ; y = 4 ; z = x * y', function () { 623 | var parser = new Parser(); 624 | assert.strictEqual(parser.parse('x = 3 ; y = 4 ; z = x * y').toString(), '((x = (3));((y = (4));(z = ((x * y)))))'); 625 | }); 626 | 627 | it('2+(x=3;y=4;z=x*y)+5', function () { 628 | var parser = new Parser(); 629 | assert.strictEqual(parser.parse('2+(x=3;y=4;z=x*y)+5').toString(), '((2 + ((x = (3));((y = (4));(z = ((x * y)))))) + 5)'); 630 | }); 631 | 632 | it('[1, 2, 3]', function () { 633 | assert.strictEqual(Parser.parse('[1, 2, 3]').toString(), '[1, 2, 3]'); 634 | }); 635 | 636 | it('[1, 2, 3, [4, [5, 6]]]', function () { 637 | assert.strictEqual(Parser.parse('[1, 2, 3, [4, [5, 6]]]').toString(), '[1, 2, 3, [4, [5, 6]]]'); 638 | }); 639 | 640 | it('["a", ["b", ["c"]], true, 1 + 2 + 3]', function () { 641 | assert.strictEqual(Parser.parse('["a", ["b", ["c"]], true, 1 + 2 + 3]').toString(), '["a", ["b", ["c"]], true, ((1 + 2) + 3)]'); 642 | }); 643 | 644 | it('\'as\' || \'df\'', function () { 645 | assert.strictEqual(parser.parse('\'as\' || \'df\'').toString(), '("as" || "df")'); 646 | }); 647 | 648 | it('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'', function () { 649 | assert.strictEqual(parser.parse('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'').toString(), '"A\\bB\\tC\\nD\\fE\\r\'F\\\\G"'); 650 | }); 651 | 652 | it('negative numbers are parenthesized', function () { 653 | assert.strictEqual(parser.parse('x + y').simplify({ y: -2 }).toString(), '(x + (-2))'); 654 | assert.strictEqual(parser.parse('x + (2 - 3)').simplify().toString(), '(x + (-1))'); 655 | }); 656 | 657 | it('(x - 1)!', function () { 658 | assert.strictEqual(parser.parse('(x - 1)!').toString(), '((x - 1)!)'); 659 | }); 660 | 661 | it('a[0]', function () { 662 | assert.strictEqual(parser.parse('a[0]').toString(), 'a[0]'); 663 | }); 664 | 665 | it('a[2 + 3]', function () { 666 | assert.strictEqual(parser.parse('a[2 + 3]').toString(), 'a[(2 + 3)]'); 667 | }); 668 | 669 | it('[1, 2+3, a, "5"]', function () { 670 | assert.strictEqual(parser.parse('[1, 2+3, a, "5"]').toString(), '[1, (2 + 3), a, "5"]'); 671 | }); 672 | }); 673 | 674 | describe('toJSFunction()', function () { 675 | var parser = new Parser(); 676 | 677 | it('2 ^ x', function () { 678 | var expr = parser.parse('2 ^ x'); 679 | var f = expr.toJSFunction('x'); 680 | assert.strictEqual(f(2), 4); 681 | assert.strictEqual(f(3), 8); 682 | assert.strictEqual(f(-1), 0.5); 683 | }); 684 | 685 | it('x || y', function () { 686 | var expr = parser.parse('x || y'); 687 | var f = expr.toJSFunction('x, y'); 688 | assert.strictEqual(f(4, 2), '42'); 689 | }); 690 | 691 | it('[4, 3] || [1, 2]', function () { 692 | var expr = parser.parse('x || y'); 693 | var f = expr.toJSFunction('x, y'); 694 | assert.deepStrictEqual(f([ 4, 3 ], [ 1, 2 ]), [ 4, 3, 1, 2 ]); 695 | }); 696 | 697 | it('x = x + 1', function () { 698 | var expr = parser.parse('x = x + 1'); 699 | var f = expr.toJSFunction('x'); 700 | assert.strictEqual(f(4), 5); 701 | }); 702 | 703 | it('y = 4 ; z = x < 5 ? x * y : x / y', function () { 704 | var expr = parser.parse('y = 4 ; z = x < 5 ? x * y : x / y'); 705 | var f = expr.toJSFunction('x'); 706 | assert.strictEqual(f(3), 12); 707 | }); 708 | 709 | it('(sqrt y) + max(3, 1) * (x ? -y : z)', function () { 710 | var expr = parser.parse('(sqrt y) + max(3, 1) * (x ? -y : z)'); 711 | var f = expr.toJSFunction('x,y,z'); 712 | assert.strictEqual(f(true, 4, 3), -10); 713 | assert.strictEqual(f(false, 4, 3), 11); 714 | }); 715 | 716 | it('should throw when missing parameter', function () { 717 | var expr = parser.parse('x * (y * atan(1))'); 718 | var f = expr.toJSFunction(['x', 'y']); 719 | assert.strictEqual(f(2, 4), 6.283185307179586); 720 | 721 | f = expr.toJSFunction(['y']); 722 | assert.throws(function () { return f(4); }, Error); 723 | }); 724 | 725 | it('should simplify first', function () { 726 | var expr = parser.parse('x * (y * atan(1))'); 727 | var f = expr.toJSFunction(['y'], { x: 2 }); 728 | assert.strictEqual(f(4), 6.283185307179586); 729 | }); 730 | 731 | it('2 * x + 1', function () { 732 | assert.strictEqual(parser.parse('2 * x + 1').toJSFunction('x')(4), 9); 733 | }); 734 | 735 | it('2 + 3 * x', function () { 736 | assert.strictEqual(parser.parse('2 + 3 * x').toJSFunction('x')(5), 17); 737 | }); 738 | 739 | it('2-3^x', function () { 740 | assert.strictEqual(parser.parse('2-3^x').toJSFunction('x')(2), -7); 741 | }); 742 | 743 | it('-2-3^x', function () { 744 | assert.strictEqual(parser.parse('-2-3^x').toJSFunction('x')(2), -11); 745 | }); 746 | 747 | it('-3^x', function () { 748 | assert.strictEqual(parser.parse('-3^x').toJSFunction('x')(4), -81); 749 | }); 750 | 751 | it('(-3)^x', function () { 752 | assert.strictEqual(parser.parse('(-3)^x').toJSFunction('x')(4), 81); 753 | }); 754 | 755 | it('2 ^ x.y', function () { 756 | assert.strictEqual(parser.parse('2^x.y').toJSFunction('x')({ y: 5 }), 32); 757 | }); 758 | 759 | it('2 + 3 * foo.bar.baz', function () { 760 | assert.strictEqual(parser.parse('2 + 3 * foo.bar.baz').toJSFunction('foo')({ bar: { baz: 5 } }), 17); 761 | }); 762 | 763 | it('sqrt 10/-1', function () { 764 | assert.strictEqual(parser.parse('sqrt 10/-1').toJSFunction()(), -Math.sqrt(10)); 765 | }); 766 | 767 | it('10*-1', function () { 768 | assert.strictEqual(parser.parse('10*-1').toJSFunction()(), -10); 769 | }); 770 | 771 | it('10+-1', function () { 772 | assert.strictEqual(parser.parse('10+-1').toJSFunction()(), 9); 773 | }); 774 | 775 | it('10+ +1', function () { 776 | assert.strictEqual(parser.parse('10+ +1').toJSFunction()(), 11); 777 | }); 778 | 779 | it('sin 2^-4', function () { 780 | assert.strictEqual(parser.parse('sin 2^-4').toJSFunction('x')(4), Math.sin(1 / 16)); 781 | }); 782 | 783 | it('a ? b : c', function () { 784 | assert.strictEqual(parser.parse('a ? b : c').toJSFunction('a,b,c')(1, 2, 3), 2); 785 | assert.strictEqual(parser.parse('a ? b : c').toJSFunction('a,b,c')(0, 2, 3), 3); 786 | }); 787 | 788 | it('a ? b : c ? d : e', function () { 789 | assert.strictEqual(parser.parse('a ? b : c ? d : e').toJSFunction('a,b,c,d,e')(1, 2, 3, 4, 5), 2); 790 | assert.strictEqual(parser.parse('a ? b : c ? d : e').toJSFunction('a,b,c,d,e')(0, 2, 3, 4, 5), 4); 791 | assert.strictEqual(parser.parse('a ? b : c ? d : e').toJSFunction('a,b,c,d,e')(0, 2, 0, 4, 5), 5); 792 | assert.strictEqual(parser.parse('a ? b : c ? d : e').toJSFunction('a,b,c,d,e')(1, 2, 0, 4, 5), 2); 793 | }); 794 | 795 | it('a ? b ? c : d : e', function () { 796 | assert.strictEqual(parser.parse('a ? b ? c : d : e').toJSFunction('a,b,c,d,e')(1, 2, 3, 4, 5), 3); 797 | assert.strictEqual(parser.parse('a ? b ? c : d : e').toJSFunction('a,b,c,d,e')(0, 2, 3, 4, 5), 5); 798 | assert.strictEqual(parser.parse('a ? b ? c : d : e').toJSFunction('a,b,c,d,e')(1, 0, 3, 4, 5), 4); 799 | assert.strictEqual(parser.parse('a ? b ? c : d : e').toJSFunction('a,b,c,d,e')(0, 0, 3, 4, 5), 5); 800 | }); 801 | 802 | it('a == 2 ? b + 1 : c * 2', function () { 803 | assert.strictEqual(parser.parse('a == 2 ? b + 1 : c * 2').toJSFunction('a,b,c')(2, 4, 8), 5); 804 | assert.strictEqual(parser.parse('a == 2 ? b + 1 : c * 2').toJSFunction('a,b,c')(1, 4, 8), 16); 805 | assert.strictEqual(parser.parse('a == 2 ? b + 1 : c * 2').toJSFunction('a,b,c')('2', 4, 8), 16); 806 | }); 807 | 808 | it('floor(random() * 10)', function () { 809 | it('should return different numbers', function () { 810 | var fn = Parser.parse('floor(random() * 10)').toJSFunction(); 811 | var counts = {}; 812 | for (var i = 0; i < 1000; i++) { 813 | var x = fn(); 814 | counts[x] = (counts[x] || 0) + 1; 815 | } 816 | for (i = 0; i < 10; i++) { 817 | assert.ok(counts[i] >= 85 && counts[i] <= 115); 818 | } 819 | assert.deepStrictEqual(Object.keys(counts).sort(), ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']); 820 | }); 821 | }); 822 | 823 | it('hypot(f(), max(2, x, y))', function () { 824 | assert.strictEqual(parser.parse('hypot(f(), max(2, x, y))').toJSFunction('f, x, y')(function () { return 3; }, 4, 1), 5); 825 | }); 826 | 827 | it('not x or y and z', function () { 828 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(0, 0, 0), true); 829 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(0, 0, 1), true); 830 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(0, 1, 0), true); 831 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(0, 1, 1), true); 832 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(1, 0, 0), false); 833 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(1, 0, 1), false); 834 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(1, 1, 0), false); 835 | assert.strictEqual(parser.parse('not x or y and z').toJSFunction('x,y,z')(1, 1, 1), true); 836 | }); 837 | 838 | it('a < b or c > d', function () { 839 | assert.strictEqual(parser.parse('a < b or c > d').toJSFunction('a,b,c,d')(1, 2, 3, 4), true); 840 | assert.strictEqual(parser.parse('a < b or c > d').toJSFunction('a,b,c,d')(2, 2, 3, 4), false); 841 | assert.strictEqual(parser.parse('a < b or c > d').toJSFunction('a,b,c,d')(2, 2, 5, 4), true); 842 | }); 843 | 844 | it('e <= f or g >= h', function () { 845 | assert.strictEqual(parser.parse('e <= f or g >= h').toJSFunction('e,f,g,h')(1, 2, 3, 4), true); 846 | assert.strictEqual(parser.parse('e <= f or g >= h').toJSFunction('e,f,g,h')(2, 2, 3, 4), true); 847 | assert.strictEqual(parser.parse('e <= f or g >= h').toJSFunction('e,f,g,h')(3, 2, 5, 4), true); 848 | assert.strictEqual(parser.parse('e <= f or g >= h').toJSFunction('e,f,g,h')(3, 2, 4, 4), true); 849 | assert.strictEqual(parser.parse('e <= f or g >= h').toJSFunction('e,f,g,h')(3, 2, 3, 4), false); 850 | }); 851 | 852 | it('i == j or k != l', function () { 853 | assert.strictEqual(parser.parse('i == j or k != l').toJSFunction('i,j,k,l')(1, 2, 3, 4), true); 854 | assert.strictEqual(parser.parse('i == j or k != l').toJSFunction('i,j,k,l')(2, 2, 3, 4), true); 855 | assert.strictEqual(parser.parse('i == j or k != l').toJSFunction('i,j,k,l')(1, 2, 4, 4), false); 856 | assert.strictEqual(parser.parse('i == j or k != l').toJSFunction('i,j,k,l')('2', 2, 4, 4), false); 857 | assert.strictEqual(parser.parse('i == j or k != l').toJSFunction('i,j,k,l')('2', 2, '4', 4), true); 858 | }); 859 | 860 | it('short-circuits and', function () { 861 | assert.strictEqual(parser.parse('a and fail()').toJSFunction('a')(false), false); 862 | }); 863 | 864 | it('short-circuits or', function () { 865 | assert.strictEqual(parser.parse('a or fail()').toJSFunction('a')(true), true); 866 | }); 867 | 868 | it('\'as\' || s', function () { 869 | assert.strictEqual(parser.parse('\'as\' || s').toJSFunction('s')('df'), 'asdf'); 870 | assert.strictEqual(parser.parse('\'as\' || s').toJSFunction('s')(4), 'as4'); 871 | }); 872 | 873 | it('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'', function () { 874 | assert.strictEqual(parser.parse('\'A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G\'').toJSFunction()(), 'A\bB\tC\nD\fE\r\'F\\G'); 875 | }); 876 | 877 | it('"A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G"', function () { 878 | assert.strictEqual(parser.parse('"A\\bB\\tC\\nD\\fE\\r\\\'F\\\\G"').toJSFunction()(), 'A\bB\tC\nD\fE\r\'F\\G'); 879 | }); 880 | 881 | it('"\\u2028 and \\u2029"', function () { 882 | assert.strictEqual(parser.parse('"\\u2028 and \\u2029 \\u2028\\u2029"').toJSFunction()(), '\u2028 and \u2029 \u2028\u2029'); 883 | }); 884 | 885 | it('(x - 1)!', function () { 886 | assert.strictEqual(parser.parse('(x - 1)!').toJSFunction('x')(1), 1); 887 | assert.strictEqual(parser.parse('(x - 1)!').toJSFunction('x')(2), 1); 888 | assert.strictEqual(parser.parse('(x - 1)!').toJSFunction('x')(3), 2); 889 | assert.strictEqual(parser.parse('(x - 1)!').toJSFunction('x')(4), 6); 890 | assert.strictEqual(parser.parse('(x - 1)!').toJSFunction('x')(5), 24); 891 | assert.strictEqual(parser.parse('(x - 1)!').toJSFunction('x')(6), 120); 892 | }); 893 | 894 | it('(f(x) = g(y) = x * y)(a)(b)', function () { 895 | var f = parser.parse('(f(x) = g(y) = x * y)(a)(b)').toJSFunction('a,b'); 896 | assert.strictEqual(f(3, 4), 12); 897 | assert.strictEqual(f(4, 5), 20); 898 | }); 899 | 900 | it('[x, y, z]', function () { 901 | assert.deepStrictEqual(parser.parse('[x, y, z]').toJSFunction('x,y,z')(1, 2, 3), [1, 2, 3]); 902 | }); 903 | 904 | it('[x, [y, [z]]]', function () { 905 | assert.deepStrictEqual(parser.parse('[x, [y, [z]]]').toJSFunction('x,y,z')('abc', true, 3), ['abc', [true, [3]]]); 906 | }); 907 | 908 | it('a[2]', function () { 909 | assert.strictEqual(parser.parse('a[2]').toJSFunction('a')([ 1, 2, 3 ]), 3); 910 | }); 911 | 912 | it('a[2.9]', function () { 913 | assert.strictEqual(parser.parse('a[2.9]').toJSFunction('a')([ 1, 2, 3, 4, 5 ]), 3); 914 | }); 915 | 916 | it('a[n]', function () { 917 | assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 0), 1); 918 | assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 1), 2); 919 | assert.strictEqual(parser.parse('a[n]').toJSFunction('a,n')([ 1, 2, 3 ], 2), 3); 920 | }); 921 | 922 | it('a["foo"]', function () { 923 | assert.strictEqual(parser.parse('a["foo"]').toJSFunction('a')({ foo: 42 }), undefined); 924 | }); 925 | 926 | it('[1, 2+3, 4*5, 6/7, [8, 9, 10], "1" || "1"]', function () { 927 | var exp = parser.parse('[1, 2+3, 4*5, 6/7, [8, 9, 10], "1" || "1"]'); 928 | assert.strictEqual(JSON.stringify(exp.toJSFunction()()), JSON.stringify([1, 5, 20, 6 / 7, [8, 9, 10], '11'])); 929 | }); 930 | }); 931 | }); 932 | -------------------------------------------------------------------------------- /test/operators.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | 'use strict'; 4 | 5 | var assert = require('assert'); 6 | var Parser = require('../dist/bundle').Parser; 7 | var spy = require('./lib/spy'); 8 | 9 | function returnTrue() { 10 | return true; 11 | } 12 | 13 | function returnFalse() { 14 | return false; 15 | } 16 | 17 | function assertCloseTo(expected, actual, delta) { 18 | return assert.ok(Math.abs(expected - actual) <= delta); 19 | } 20 | 21 | describe('Operators', function () { 22 | var parser = new Parser(); 23 | 24 | describe('== operator', function () { 25 | it('2 == 3', function () { 26 | assert.strictEqual(Parser.evaluate('2 == 3'), false); 27 | }); 28 | 29 | it('3 * 1 == 2', function () { 30 | assert.strictEqual(Parser.evaluate('3 == 2'), false); 31 | }); 32 | 33 | it('3 == 3', function () { 34 | assert.strictEqual(Parser.evaluate('3 == 3'), true); 35 | }); 36 | 37 | it('\'3\' == 3', function () { 38 | assert.strictEqual(Parser.evaluate('\'3\' == 3'), false); 39 | }); 40 | 41 | it('\'string 1\' == \'string 2\'', function () { 42 | assert.strictEqual(Parser.evaluate('\'string 1\' == \'string 2\''), false); 43 | }); 44 | 45 | it('\'string 1\' == "string 1"', function () { 46 | assert.strictEqual(Parser.evaluate('\'string 1\' == \'string 1\''), true); 47 | }); 48 | 49 | it('\'3\' == \'3\'', function () { 50 | assert.strictEqual(Parser.evaluate('\'3\' == \'3\''), true); 51 | }); 52 | }); 53 | 54 | describe('!= operator', function () { 55 | it('2 != 3', function () { 56 | assert.strictEqual(Parser.evaluate('2 != 3'), true); 57 | }); 58 | 59 | it('3 != 2', function () { 60 | assert.strictEqual(Parser.evaluate('3 != 2'), true); 61 | }); 62 | 63 | it('3 != 3', function () { 64 | assert.strictEqual(Parser.evaluate('3 != 3'), false); 65 | }); 66 | 67 | it('\'3\' != 3', function () { 68 | assert.strictEqual(Parser.evaluate('\'3\' != 3'), true); 69 | }); 70 | 71 | it('\'3\' != \'3\'', function () { 72 | assert.strictEqual(Parser.evaluate('\'3\' != \'3\''), false); 73 | }); 74 | 75 | it('\'string 1\' != \'string 1\'', function () { 76 | assert.strictEqual(Parser.evaluate('\'string 1\' != \'string 1\''), false); 77 | }); 78 | 79 | it('\'string 1\' != \'string 2\'', function () { 80 | assert.strictEqual(Parser.evaluate('\'string 1\' != \'string 2\''), true); 81 | }); 82 | }); 83 | 84 | describe('> operator', function () { 85 | it('2 > 3', function () { 86 | assert.strictEqual(Parser.evaluate('2 > 3'), false); 87 | }); 88 | 89 | it('3 > 2', function () { 90 | assert.strictEqual(Parser.evaluate('3 > 2'), true); 91 | }); 92 | 93 | it('3 > 3', function () { 94 | assert.strictEqual(Parser.evaluate('3 > 3'), false); 95 | }); 96 | }); 97 | 98 | describe('>= operator', function () { 99 | it('2 >= 3', function () { 100 | assert.strictEqual(Parser.evaluate('2 >= 3'), false); 101 | }); 102 | 103 | it('3 >= 2', function () { 104 | assert.strictEqual(Parser.evaluate('3 >= 2'), true); 105 | }); 106 | 107 | it('3 >= 3', function () { 108 | assert.strictEqual(Parser.evaluate('3 >= 3'), true); 109 | }); 110 | }); 111 | 112 | describe('< operator', function () { 113 | it('2 < 3', function () { 114 | assert.strictEqual(Parser.evaluate('2 < 3'), true); 115 | }); 116 | 117 | it('3 < 2', function () { 118 | assert.strictEqual(Parser.evaluate('3 < 2'), false); 119 | }); 120 | 121 | it('3 < 3', function () { 122 | assert.strictEqual(Parser.evaluate('3 < 3'), false); 123 | }); 124 | }); 125 | 126 | describe('<= operator', function () { 127 | it('2 <= 3', function () { 128 | assert.strictEqual(Parser.evaluate('2 <= 3'), true); 129 | }); 130 | 131 | it('3 <= 2', function () { 132 | assert.strictEqual(Parser.evaluate('3 <= 2'), false); 133 | }); 134 | 135 | it('3 <= 3', function () { 136 | assert.strictEqual(Parser.evaluate('3 <= 3'), true); 137 | }); 138 | }); 139 | 140 | describe('and operator', function () { 141 | it('1 and 0', function () { 142 | assert.strictEqual(Parser.evaluate('1 and 0'), false); 143 | }); 144 | 145 | it('1 and 1', function () { 146 | assert.strictEqual(Parser.evaluate('1 and 1'), true); 147 | }); 148 | 149 | it('0 and 0', function () { 150 | assert.strictEqual(Parser.evaluate('0 and 0'), false); 151 | }); 152 | 153 | it('0 and 1', function () { 154 | assert.strictEqual(Parser.evaluate('0 and 1'), false); 155 | }); 156 | 157 | it('0 and 1 and 0', function () { 158 | assert.strictEqual(Parser.evaluate('0 and 1 and 0'), false); 159 | }); 160 | 161 | it('1 and 1 and 0', function () { 162 | assert.strictEqual(Parser.evaluate('1 and 1 and 0'), false); 163 | }); 164 | 165 | it('skips rhs when lhs is false', function () { 166 | var notCalled = spy(returnFalse); 167 | 168 | assert.strictEqual(Parser.evaluate('false and notCalled()', { notCalled: notCalled }), false); 169 | assert.strictEqual(notCalled.called, false); 170 | }); 171 | 172 | it('evaluates rhs when lhs is true', function () { 173 | var called = spy(returnFalse); 174 | 175 | assert.strictEqual(Parser.evaluate('true and called()', { called: called }), false); 176 | assert.strictEqual(called.called, true); 177 | }); 178 | }); 179 | 180 | describe('or operator', function () { 181 | it('1 or 0', function () { 182 | assert.strictEqual(Parser.evaluate('1 or 0'), true); 183 | }); 184 | 185 | it('1 or 1', function () { 186 | assert.strictEqual(Parser.evaluate('1 or 1'), true); 187 | }); 188 | 189 | it('0 or 0', function () { 190 | assert.strictEqual(Parser.evaluate('0 or 0'), false); 191 | }); 192 | 193 | it('0 or 1', function () { 194 | assert.strictEqual(Parser.evaluate('0 or 1'), true); 195 | }); 196 | 197 | it('0 or 1 or 0', function () { 198 | assert.strictEqual(Parser.evaluate('0 or 1 or 0'), true); 199 | }); 200 | 201 | it('1 or 1 or 0', function () { 202 | assert.strictEqual(Parser.evaluate('1 or 1 or 0'), true); 203 | }); 204 | 205 | it('skips rhs when lhs is true', function () { 206 | var notCalled = spy(returnFalse); 207 | 208 | assert.strictEqual(Parser.evaluate('true or notCalled()', { notCalled: notCalled }), true); 209 | assert.strictEqual(notCalled.called, false); 210 | }); 211 | 212 | it('evaluates rhs when lhs is false', function () { 213 | var called = spy(returnTrue); 214 | 215 | assert.strictEqual(Parser.evaluate('false or called()', { called: called }), true); 216 | assert.strictEqual(called.called, true); 217 | }); 218 | }); 219 | 220 | describe('in operator', function () { 221 | var parser = new Parser(); 222 | 223 | it('"a" in ["a", "b"]', function () { 224 | assert.strictEqual(parser.evaluate('"a" in toto', { 'toto': ['a', 'b'] }), true); 225 | }); 226 | 227 | it('"a" in ["b", "a"]', function () { 228 | assert.strictEqual(parser.evaluate('"a" in toto', { 'toto': ['b', 'a'] }), true); 229 | }); 230 | 231 | it('3 in [4, 3]', function () { 232 | assert.strictEqual(parser.evaluate('3 in toto', { 'toto': [4, 3] }), true); 233 | }); 234 | 235 | it('"c" in ["a", "b"]', function () { 236 | assert.strictEqual(parser.evaluate('"c" in toto', { 'toto': ['a', 'b'] }), false); 237 | }); 238 | 239 | it('"c" in ["b", "a"]', function () { 240 | assert.strictEqual(parser.evaluate('"c" in toto', { 'toto': ['b', 'a'] }), false); 241 | }); 242 | 243 | it('3 in [1, 2]', function () { 244 | assert.strictEqual(parser.evaluate('3 in toto', { 'toto': [1, 2] }), false); 245 | }); 246 | }); 247 | 248 | describe('not operator', function () { 249 | it('not 1', function () { 250 | assert.strictEqual(Parser.evaluate('not 1'), false); 251 | }); 252 | 253 | it('not true', function () { 254 | assert.strictEqual(Parser.evaluate('not true'), false); 255 | }); 256 | 257 | it('not 0', function () { 258 | assert.strictEqual(Parser.evaluate('not 0'), true); 259 | }); 260 | 261 | it('not false', function () { 262 | assert.strictEqual(Parser.evaluate('not false'), true); 263 | }); 264 | 265 | it('not 4', function () { 266 | assert.strictEqual(Parser.evaluate('not 4'), false); 267 | }); 268 | 269 | it('1 and not 0', function () { 270 | assert.strictEqual(Parser.evaluate('1 and not 0'), true); 271 | }); 272 | 273 | it('not \'0\'', function () { 274 | assert.strictEqual(Parser.evaluate('not \'0\''), false); 275 | }); 276 | 277 | it('not \'\'', function () { 278 | assert.strictEqual(Parser.evaluate('not \'\''), true); 279 | }); 280 | }); 281 | 282 | describe('conditional operator', function () { 283 | var parser = new Parser(); 284 | 285 | it('1 ? 2 : 0 ? 3 : 4', function () { 286 | assert.strictEqual(parser.evaluate('1 ? 2 : 0 ? 3 : 4'), 2); 287 | }); 288 | 289 | it('(1 ? 2 : 0) ? 3 : 4', function () { 290 | assert.strictEqual(parser.evaluate('(1 ? 2 : 0) ? 3 : 4'), 3); 291 | }); 292 | 293 | it('0 ? 2 : 0 ? 3 : 4', function () { 294 | assert.strictEqual(parser.evaluate('0 ? 2 : 0 ? 3 : 4'), 4); 295 | }); 296 | 297 | it('(0 ? 2 : 0) ? 3 : 4', function () { 298 | assert.strictEqual(parser.evaluate('0 ? 2 : 0 ? 3 : 4'), 4); 299 | }); 300 | 301 | it('(0 ? 0 : 2) ? 3 : 4', function () { 302 | assert.strictEqual(parser.evaluate('(1 ? 2 : 0) ? 3 : 4'), 3); 303 | }); 304 | 305 | it('min(1 ? 3 : 10, 0 ? 11 : 2)', function () { 306 | assert.strictEqual(parser.evaluate('min(1 ? 3 : 10, 0 ? 11 : 2)'), 2); 307 | }); 308 | 309 | it('a == 1 ? b == 2 ? 3 : 4 : 5', function () { 310 | assert.strictEqual(parser.evaluate('a == 1 ? b == 2 ? 3 : 4 : 5', { a: 1, b: 2 }), 3); 311 | assert.strictEqual(parser.evaluate('a == 1 ? b == 2 ? 3 : 4 : 5', { a: 1, b: 9 }), 4); 312 | assert.strictEqual(parser.evaluate('a == 1 ? b == 2 ? 3 : 4 : 5', { a: 9, b: 2 }), 5); 313 | assert.strictEqual(parser.evaluate('a == 1 ? b == 2 ? 3 : 4 : 5', { a: 9, b: 9 }), 5); 314 | }); 315 | 316 | it('should only evaluate one branch', function () { 317 | assert.strictEqual(parser.evaluate('1 ? 42 : fail'), 42); 318 | assert.strictEqual(parser.evaluate('0 ? fail : 99'), 99); 319 | }); 320 | }); 321 | 322 | describe('length operator', function () { 323 | var parser = new Parser(); 324 | 325 | it('should return 0 for empty strings', function () { 326 | assert.strictEqual(parser.evaluate('length ""'), 0); 327 | }); 328 | 329 | it('should return the length of a string', function () { 330 | assert.strictEqual(parser.evaluate('length "a"'), 1); 331 | assert.strictEqual(parser.evaluate('length "as"'), 2); 332 | assert.strictEqual(parser.evaluate('length "asd"'), 3); 333 | assert.strictEqual(parser.evaluate('length "asdf"'), 4); 334 | }); 335 | 336 | it('should convert numbers to strings', function () { 337 | assert.strictEqual(parser.evaluate('length 0'), 1); 338 | assert.strictEqual(parser.evaluate('length 12'), 2); 339 | assert.strictEqual(parser.evaluate('length 999'), 3); 340 | assert.strictEqual(parser.evaluate('length 1000'), 4); 341 | assert.strictEqual(parser.evaluate('length -1'), 2); 342 | assert.strictEqual(parser.evaluate('length -999'), 4); 343 | }); 344 | 345 | it('should return 0 for empty arrays', function () { 346 | assert.strictEqual(parser.evaluate('length []'), 0); 347 | }); 348 | 349 | it('should return the length of an array', function () { 350 | assert.strictEqual(parser.evaluate('length [123]'), 1); 351 | assert.strictEqual(parser.evaluate('length [123, 456]'), 2); 352 | assert.strictEqual(parser.evaluate('length [12, 34, 56]'), 3); 353 | assert.strictEqual(parser.evaluate('length [1, 2, 3, 4]'), 4); 354 | }); 355 | }); 356 | 357 | describe('% operator', function () { 358 | it('has the correct precedence', function () { 359 | assert.strictEqual(parser.parse('a + b % c ^ d').toString(), '(a + (b % (c ^ d)))'); 360 | assert.strictEqual(parser.parse('a + b * c % d').toString(), '(a + ((b * c) % d))'); 361 | assert.strictEqual(parser.parse('a + b % c * d').toString(), '(a + ((b % c) * d))'); 362 | assert.strictEqual(parser.parse('a + b % c % d').toString(), '(a + ((b % c) % d))'); 363 | }); 364 | 365 | it('returns the correct value', function () { 366 | assert.strictEqual(parser.evaluate('0 % 5'), 0); 367 | assert.strictEqual(parser.evaluate('1 % 5'), 1); 368 | assert.strictEqual(parser.evaluate('2 % 5'), 2); 369 | assert.strictEqual(parser.evaluate('3 % 5'), 3); 370 | assert.strictEqual(parser.evaluate('4 % 5'), 4); 371 | assert.strictEqual(parser.evaluate('5 % 5'), 0); 372 | assert.strictEqual(parser.evaluate('6 % 5'), 1); 373 | assert.strictEqual(parser.evaluate('-2 % 5'), -2); 374 | assert.strictEqual(parser.evaluate('-6 % 5'), -1); 375 | }); 376 | 377 | it('returns NaN for 0 divisor', function () { 378 | assert.ok(isNaN(parser.evaluate('0 % 0'))); 379 | assert.ok(isNaN(parser.evaluate('1 % 0'))); 380 | assert.ok(isNaN(parser.evaluate('-1 % 0'))); 381 | }); 382 | }); 383 | 384 | describe('sin(x)', function () { 385 | it('returns the correct value', function () { 386 | var delta = 1e-15; 387 | assert.strictEqual(parser.evaluate('sin 0'), 0); 388 | assertCloseTo(parser.evaluate('sin 0.5'), 0.479425538604203, delta); 389 | assertCloseTo(parser.evaluate('sin 1'), 0.8414709848078965, delta); 390 | assertCloseTo(parser.evaluate('sin -1'), -0.8414709848078965, delta); 391 | assertCloseTo(parser.evaluate('sin(PI/4)'), 0.7071067811865475, delta); 392 | assertCloseTo(parser.evaluate('sin(PI/2)'), 1, delta); 393 | assertCloseTo(parser.evaluate('sin(3*PI/4)'), 0.7071067811865475, delta); 394 | assertCloseTo(parser.evaluate('sin PI'), 0, delta); 395 | assertCloseTo(parser.evaluate('sin(2*PI)'), 0, delta); 396 | assertCloseTo(parser.evaluate('sin(-PI)'), 0, delta); 397 | assertCloseTo(parser.evaluate('sin(3*PI/2)'), -1, delta); 398 | assertCloseTo(parser.evaluate('sin 15'), 0.6502878401571168, delta); 399 | }); 400 | }); 401 | 402 | describe('cos(x)', function () { 403 | it('returns the correct value', function () { 404 | var delta = 1e-15; 405 | assert.strictEqual(parser.evaluate('cos 0'), 1); 406 | assertCloseTo(parser.evaluate('cos 0.5'), 0.8775825618903728, delta); 407 | assertCloseTo(parser.evaluate('cos 1'), 0.5403023058681398, delta); 408 | assertCloseTo(parser.evaluate('cos -1'), 0.5403023058681398, delta); 409 | assertCloseTo(parser.evaluate('cos(PI/4)'), 0.7071067811865475, delta); 410 | assertCloseTo(parser.evaluate('cos(PI/2)'), 0, delta); 411 | assertCloseTo(parser.evaluate('cos(3*PI/4)'), -0.7071067811865475, delta); 412 | assertCloseTo(parser.evaluate('cos PI'), -1, delta); 413 | assertCloseTo(parser.evaluate('cos(2*PI)'), 1, delta); 414 | assertCloseTo(parser.evaluate('cos -PI'), -1, delta); 415 | assertCloseTo(parser.evaluate('cos(3*PI/2)'), 0, delta); 416 | assertCloseTo(parser.evaluate('cos 15'), -0.7596879128588213, delta); 417 | }); 418 | }); 419 | 420 | describe('tan(x)', function () { 421 | it('returns the correct value', function () { 422 | var delta = 1e-15; 423 | assert.strictEqual(parser.evaluate('tan 0'), 0); 424 | assertCloseTo(parser.evaluate('tan 0.5'), 0.5463024898437905, delta); 425 | assertCloseTo(parser.evaluate('tan 1'), 1.5574077246549023, delta); 426 | assertCloseTo(parser.evaluate('tan -1'), -1.5574077246549023, delta); 427 | assertCloseTo(parser.evaluate('tan(PI/4)'), 1, delta); 428 | assert.ok(parser.evaluate('tan(PI/2)') > 1e16); 429 | assertCloseTo(parser.evaluate('tan(3*PI/4)'), -1, delta); 430 | assertCloseTo(parser.evaluate('tan PI'), 0, delta); 431 | assertCloseTo(parser.evaluate('tan(2*PI)'), 0, delta); 432 | assertCloseTo(parser.evaluate('tan -PI'), 0, delta); 433 | assert.ok(parser.evaluate('tan(3*PI/2)') > 1e15); 434 | assertCloseTo(parser.evaluate('tan 15'), -0.8559934009085188, delta); 435 | }); 436 | }); 437 | 438 | describe('asin(x)', function () { 439 | it('returns the correct value', function () { 440 | var delta = 1e-15; 441 | assert.strictEqual(parser.evaluate('asin 0'), 0); 442 | assertCloseTo(parser.evaluate('asin 0.5'), 0.5235987755982989, delta); 443 | assertCloseTo(parser.evaluate('asin -0.5'), -0.5235987755982989, delta); 444 | assertCloseTo(parser.evaluate('asin 1'), Math.PI / 2, delta); 445 | assertCloseTo(parser.evaluate('asin -1'), -Math.PI / 2, delta); 446 | assertCloseTo(parser.evaluate('asin(PI/4)'), 0.9033391107665127, delta); 447 | assert.ok(isNaN(parser.evaluate('asin 1.1'))); 448 | assert.ok(isNaN(parser.evaluate('asin -1.1'))); 449 | }); 450 | }); 451 | 452 | describe('acos(x)', function () { 453 | it('returns the correct value', function () { 454 | var delta = 1e-15; 455 | assert.strictEqual(parser.evaluate('acos 0'), Math.PI / 2); 456 | assertCloseTo(parser.evaluate('acos 0.5'), 1.0471975511965979, delta); 457 | assertCloseTo(parser.evaluate('acos -0.5'), 2.0943951023931957, delta); 458 | assertCloseTo(parser.evaluate('acos 1'), 0, delta); 459 | assertCloseTo(parser.evaluate('acos -1'), Math.PI, delta); 460 | assertCloseTo(parser.evaluate('acos(PI/4)'), 0.6674572160283838, delta); 461 | assert.ok(isNaN(parser.evaluate('acos 1.1'))); 462 | assert.ok(isNaN(parser.evaluate('acos -1.1'))); 463 | }); 464 | }); 465 | 466 | describe('atan(x)', function () { 467 | it('returns the correct value', function () { 468 | var delta = 1e-15; 469 | assert.strictEqual(parser.evaluate('atan 0'), 0); 470 | assertCloseTo(parser.evaluate('atan 0.5'), 0.4636476090008061, delta); 471 | assertCloseTo(parser.evaluate('atan -0.5'), -0.4636476090008061, delta); 472 | assertCloseTo(parser.evaluate('atan 1'), Math.PI / 4, delta); 473 | assertCloseTo(parser.evaluate('atan -1'), -Math.PI / 4, delta); 474 | assertCloseTo(parser.evaluate('atan(PI/4)'), 0.6657737500283538, delta); 475 | assertCloseTo(parser.evaluate('atan PI'), 1.2626272556789118, delta); 476 | assertCloseTo(parser.evaluate('atan -PI'), -1.2626272556789118, delta); 477 | assert.strictEqual(parser.evaluate('atan(1/0)'), Math.PI / 2); 478 | assert.strictEqual(parser.evaluate('atan(-1/0)'), -Math.PI / 2); 479 | assertCloseTo(parser.evaluate('atan 10'), 1.4711276743037347, delta); 480 | assertCloseTo(parser.evaluate('atan 100'), 1.5607966601082315, delta); 481 | assertCloseTo(parser.evaluate('atan 1000'), 1.5697963271282298, delta); 482 | assertCloseTo(parser.evaluate('atan 2000'), 1.5702963268365633, delta); 483 | }); 484 | }); 485 | 486 | describe('sinh(x)', function () { 487 | it('returns the correct value', function () { 488 | var delta = 1e-15; 489 | assert.strictEqual(parser.evaluate('sinh 0'), 0); 490 | assertCloseTo(parser.evaluate('sinh 0.5'), 0.5210953054937474, delta); 491 | assertCloseTo(parser.evaluate('sinh -0.5'), -0.5210953054937474, delta); 492 | assertCloseTo(parser.evaluate('sinh 1'), 1.1752011936438014, delta); 493 | assertCloseTo(parser.evaluate('sinh -1'), -1.1752011936438014, delta); 494 | assertCloseTo(parser.evaluate('sinh(PI/4)'), 0.8686709614860095, delta); 495 | assertCloseTo(parser.evaluate('sinh(PI/2)'), 2.3012989023072947, delta); 496 | assertCloseTo(parser.evaluate('sinh(3*PI/4)'), 5.227971924677803, delta); 497 | assertCloseTo(parser.evaluate('sinh PI'), 11.548739357257748, delta * 10); 498 | assertCloseTo(parser.evaluate('sinh(2*PI)'), 267.74489404101644, delta * 1000); 499 | assertCloseTo(parser.evaluate('sinh -PI'), -11.548739357257748, delta * 10); 500 | assertCloseTo(parser.evaluate('sinh(3*PI/2)'), 55.65439759941754, delta * 100); 501 | assertCloseTo(parser.evaluate('sinh 15'), 1634508.6862359024, delta * 1000000); 502 | assert.strictEqual(parser.evaluate('sinh(1 / 0)'), Infinity); 503 | assert.strictEqual(parser.evaluate('sinh(-1 / 0)'), -Infinity); 504 | }); 505 | }); 506 | 507 | describe('cosh(x)', function () { 508 | it('returns the correct value', function () { 509 | var delta = 1e-15; 510 | assert.strictEqual(parser.evaluate('cosh 0'), 1); 511 | assertCloseTo(parser.evaluate('cosh 0.5'), 1.1276259652063807, delta); 512 | assertCloseTo(parser.evaluate('cosh -0.5'), 1.1276259652063807, delta); 513 | assertCloseTo(parser.evaluate('cosh 1'), 1.5430806348152437, delta); 514 | assertCloseTo(parser.evaluate('cosh -1'), 1.5430806348152437, delta); 515 | assertCloseTo(parser.evaluate('cosh(PI/4)'), 1.324609089252006, delta); 516 | assertCloseTo(parser.evaluate('cosh(PI/2)'), 2.509178478658057, delta); 517 | assertCloseTo(parser.evaluate('cosh(3*PI/4)'), 5.3227521495199595, delta); 518 | assertCloseTo(parser.evaluate('cosh PI'), 11.591953275521522, delta * 10); 519 | assertCloseTo(parser.evaluate('cosh(2*PI)'), 267.7467614837483, delta * 1000); 520 | assertCloseTo(parser.evaluate('cosh -PI'), 11.591953275521522, delta * 10); 521 | assertCloseTo(parser.evaluate('cosh(3*PI/2)'), 55.663380890438695, delta * 100); 522 | assertCloseTo(parser.evaluate('cosh 15'), 1634508.6862362078, delta * 1e7); 523 | assert.strictEqual(parser.evaluate('cosh(1 / 0)'), Infinity); 524 | assert.strictEqual(parser.evaluate('cosh(-1 / 0)'), Infinity); 525 | }); 526 | }); 527 | 528 | describe('tanh(x)', function () { 529 | it('returns the correct value', function () { 530 | var delta = 1e-15; 531 | assert.strictEqual(parser.evaluate('tanh 0'), 0); 532 | assertCloseTo(parser.evaluate('tanh 0.00001'), 0.000009999999999621023, delta); 533 | assertCloseTo(parser.evaluate('tanh 0.25'), 0.24491866240370924, delta); 534 | assertCloseTo(parser.evaluate('tanh -0.25'), -0.24491866240370924, delta); 535 | assertCloseTo(parser.evaluate('tanh 0.5'), 0.4621171572600098, delta); 536 | assertCloseTo(parser.evaluate('tanh -0.5'), -0.4621171572600098, delta); 537 | assertCloseTo(parser.evaluate('tanh 1'), 0.7615941559557649, delta); 538 | assertCloseTo(parser.evaluate('tanh -1'), -0.7615941559557649, delta); 539 | assertCloseTo(parser.evaluate('tanh(PI/4)'), 0.6557942026326725, delta); 540 | assertCloseTo(parser.evaluate('tanh(PI/2)'), 0.9171523356672744, delta); 541 | assertCloseTo(parser.evaluate('tanh PI'), 0.9962720762207501, delta); 542 | assertCloseTo(parser.evaluate('tanh -PI'), -0.9962720762207501, delta); 543 | assertCloseTo(parser.evaluate('tanh(2*PI)'), 0.9999930253396105, delta); 544 | assertCloseTo(parser.evaluate('tanh 15'), 0.9999999999998128, delta); 545 | assertCloseTo(parser.evaluate('tanh -15'), -0.9999999999998128, delta); 546 | assertCloseTo(parser.evaluate('tanh 16'), 0.9999999999999748, delta); 547 | assertCloseTo(parser.evaluate('tanh 17'), 0.9999999999999966, delta); 548 | assert.strictEqual(parser.evaluate('tanh 20'), 1); 549 | assert.strictEqual(parser.evaluate('tanh -20'), -1); 550 | assert.strictEqual(parser.evaluate('tanh 100'), 1); 551 | assert.strictEqual(parser.evaluate('tanh -100'), -1); 552 | assert.strictEqual(parser.evaluate('tanh(1 / 0)'), 1); 553 | assert.strictEqual(parser.evaluate('tanh(-1 / 0)'), -1); 554 | }); 555 | }); 556 | 557 | describe('asinh(x)', function () { 558 | it('returns the correct value', function () { 559 | var delta = 1e-15; 560 | assert.strictEqual(parser.evaluate('asinh 0'), 0); 561 | assertCloseTo(parser.evaluate('asinh 0.5'), 0.48121182505960347, delta); 562 | assertCloseTo(parser.evaluate('asinh -0.5'), -0.48121182505960347, delta); 563 | assertCloseTo(parser.evaluate('asinh 1'), 0.881373587019543, delta); 564 | assertCloseTo(parser.evaluate('asinh -1'), -0.881373587019543, delta); 565 | assertCloseTo(parser.evaluate('asinh(PI/4)'), 0.7212254887267799, delta); 566 | assertCloseTo(parser.evaluate('asinh(PI/2)'), 1.233403117511217, delta); 567 | assertCloseTo(parser.evaluate('asinh(3*PI/4)'), 1.5924573728585427, delta); 568 | assertCloseTo(parser.evaluate('asinh PI'), 1.8622957433108482, delta); 569 | assertCloseTo(parser.evaluate('asinh(2*PI)'), 2.537297501373361, delta); 570 | assertCloseTo(parser.evaluate('asinh -PI'), -1.8622957433108482, delta); 571 | assertCloseTo(parser.evaluate('asinh(3*PI/2)'), 2.2544145929927146, delta); 572 | assertCloseTo(parser.evaluate('asinh 15'), 3.4023066454805946, delta); 573 | assertCloseTo(parser.evaluate('asinh 100'), 5.298342365610589, delta); 574 | assertCloseTo(parser.evaluate('asinh 1000'), 7.600902709541988, delta); 575 | assert.strictEqual(parser.evaluate('asinh(1 / 0)'), Infinity); 576 | assert.strictEqual(parser.evaluate('asinh(-1 / 0)'), -Infinity); 577 | }); 578 | }); 579 | 580 | describe('acosh(x)', function () { 581 | it('returns the correct value', function () { 582 | var delta = 1e-15; 583 | assert.ok(isNaN(parser.evaluate('acosh 0'))); 584 | assert.ok(isNaN(parser.evaluate('acosh 0.5'))); 585 | assert.ok(isNaN(parser.evaluate('acosh -0.5'))); 586 | assert.ok(isNaN(parser.evaluate('acosh -1'))); 587 | assert.strictEqual(parser.evaluate('acosh 1'), 0); 588 | assertCloseTo(parser.evaluate('acosh(PI/2)'), 1.0232274785475506, delta); 589 | assertCloseTo(parser.evaluate('acosh(3*PI/4)'), 1.5017757950235857, delta); 590 | assertCloseTo(parser.evaluate('acosh PI'), 1.8115262724608532, delta); 591 | assertCloseTo(parser.evaluate('acosh(2*PI)'), 2.524630659933467, delta); 592 | assertCloseTo(parser.evaluate('acosh(3*PI/2)'), 2.2318892530580827, delta); 593 | assertCloseTo(parser.evaluate('acosh 15'), 3.4000844141133393, delta); 594 | assertCloseTo(parser.evaluate('acosh 100'), 5.298292365610485, delta); 595 | assertCloseTo(parser.evaluate('acosh 1000'), 7.600902209541989, delta); 596 | assert.strictEqual(parser.evaluate('acosh(1 / 0)'), Infinity); 597 | assert.ok(isNaN(parser.evaluate('acosh(-1 / 0)'))); 598 | }); 599 | }); 600 | 601 | describe('atanh(x)', function () { 602 | it('returns the correct value', function () { 603 | var delta = 1e-15; 604 | assert.strictEqual(parser.evaluate('atanh 0'), 0); 605 | assertCloseTo(parser.evaluate('atanh 0.25'), 0.25541281188299536, delta); 606 | assertCloseTo(parser.evaluate('atanh -0.25'), -0.25541281188299536, delta); 607 | assertCloseTo(parser.evaluate('atanh 0.5'), 0.5493061443340549, delta); 608 | assertCloseTo(parser.evaluate('atanh -0.5'), -0.5493061443340549, delta); 609 | assert.strictEqual(parser.evaluate('atanh 1'), Infinity); 610 | assert.strictEqual(parser.evaluate('atanh -1'), -Infinity); 611 | assert.ok(isNaN(parser.evaluate('atanh 1.001'))); 612 | assert.ok(isNaN(parser.evaluate('atanh -1.001'))); 613 | assert.ok(isNaN(parser.evaluate('atanh(1 / 0)'))); 614 | assert.ok(isNaN(parser.evaluate('atanh(-1 / 0)'))); 615 | }); 616 | }); 617 | 618 | describe('sqrt(x)', function () { 619 | it('returns the square root of its argument', function () { 620 | var delta = 1e-15; 621 | assert.strictEqual(parser.evaluate('sqrt 0'), 0); 622 | assertCloseTo(parser.evaluate('sqrt 0.5'), 0.7071067811865476, delta); 623 | assert.strictEqual(parser.evaluate('sqrt 1'), 1); 624 | assertCloseTo(parser.evaluate('sqrt 2'), 1.4142135623730951, delta); 625 | assert.strictEqual(parser.evaluate('sqrt 4'), 2); 626 | assertCloseTo(parser.evaluate('sqrt 8'), 2.8284271247461903, 0); 627 | assert.strictEqual(parser.evaluate('sqrt 16'), 4); 628 | assert.strictEqual(parser.evaluate('sqrt 81'), 9); 629 | assert.strictEqual(parser.evaluate('sqrt 100'), 10); 630 | assert.strictEqual(parser.evaluate('sqrt 1000'), 31.622776601683793); 631 | assert.ok(isNaN(parser.evaluate('sqrt -1'))); 632 | }); 633 | }); 634 | 635 | describe('ln/log operator', function () { 636 | it('returns the natural logarithm of its argument', function () { 637 | var delta = 1e-15; 638 | assert.strictEqual(Parser.evaluate('ln 0'), -Infinity); 639 | assert.strictEqual(Parser.evaluate('log 0'), -Infinity); 640 | assertCloseTo(Parser.evaluate('ln 0.5'), -0.6931471805599453, delta); 641 | assertCloseTo(Parser.evaluate('log 0.5'), -0.6931471805599453, delta); 642 | assert.strictEqual(Parser.evaluate('ln 1'), 0); 643 | assert.strictEqual(Parser.evaluate('log 1'), 0); 644 | assertCloseTo(Parser.evaluate('ln 2'), 0.6931471805599453, delta); 645 | assertCloseTo(Parser.evaluate('log 2'), 0.6931471805599453, delta); 646 | assert.strictEqual(Parser.evaluate('ln E'), 1); 647 | assert.strictEqual(Parser.evaluate('log E'), 1); 648 | assertCloseTo(Parser.evaluate('ln PI'), 1.1447298858494002, delta); 649 | assertCloseTo(Parser.evaluate('log PI'), 1.1447298858494002, delta); 650 | assertCloseTo(Parser.evaluate('ln 10'), 2.302585092994046, delta); 651 | assertCloseTo(Parser.evaluate('log 10'), 2.302585092994046, delta); 652 | assertCloseTo(Parser.evaluate('ln 100'), 4.605170185988092, delta); 653 | assert.ok(isNaN(Parser.evaluate('ln -1'))); 654 | assert.ok(isNaN(Parser.evaluate('log -1'))); 655 | }); 656 | }); 657 | 658 | describe('log10 operator', function () { 659 | it('returns the base-10 logarithm of its argument', function () { 660 | var delta = 1e-15; 661 | assert.strictEqual(Parser.evaluate('log10 0'), -Infinity); 662 | assert.strictEqual(Parser.evaluate('lg 0'), -Infinity); 663 | assertCloseTo(Parser.evaluate('log10 0.5'), -0.3010299956639812, delta); 664 | assertCloseTo(Parser.evaluate('lg 0.5'), -0.3010299956639812, delta); 665 | assert.strictEqual(Parser.evaluate('log10 1'), 0); 666 | assert.strictEqual(Parser.evaluate('lg 1'), 0); 667 | assertCloseTo(Parser.evaluate('log10 2'), 0.3010299956639812, delta); 668 | assertCloseTo(Parser.evaluate('lg 2'), 0.3010299956639812, delta); 669 | assertCloseTo(Parser.evaluate('log10 E'), 0.4342944819032518, delta); 670 | assertCloseTo(Parser.evaluate('lg E'), 0.4342944819032518, delta); 671 | assertCloseTo(Parser.evaluate('log10 PI'), 0.49714987269413385, delta); 672 | assertCloseTo(Parser.evaluate('lg PI'), 0.49714987269413385, delta); 673 | assert.strictEqual(Parser.evaluate('log10 10'), 1); 674 | assert.strictEqual(Parser.evaluate('lg 10'), 1); 675 | assert.strictEqual(Parser.evaluate('log10 100'), 2); 676 | assert.strictEqual(Parser.evaluate('lg 100'), 2); 677 | assert.strictEqual(Parser.evaluate('log10 1000'), 3); 678 | assert.strictEqual(Parser.evaluate('lg 1000'), 3); 679 | assert.strictEqual(Parser.evaluate('log10 10000000000'), 10); 680 | assert.strictEqual(Parser.evaluate('lg 10000000000'), 10); 681 | assert.ok(isNaN(Parser.evaluate('log10 -1'))); 682 | assert.ok(isNaN(Parser.evaluate('lg -1'))); 683 | }); 684 | }); 685 | 686 | describe('abs(x)', function () { 687 | it('returns the correct value', function () { 688 | assert.strictEqual(parser.evaluate('abs 0'), 0); 689 | assert.strictEqual(parser.evaluate('abs 0.5'), 0.5); 690 | assert.strictEqual(parser.evaluate('abs -0.5'), 0.5); 691 | assert.strictEqual(parser.evaluate('abs 1'), 1); 692 | assert.strictEqual(parser.evaluate('abs -1'), 1); 693 | assert.strictEqual(parser.evaluate('abs 2'), 2); 694 | assert.strictEqual(parser.evaluate('abs -2'), 2); 695 | assert.strictEqual(parser.evaluate('abs(-1/0)'), Infinity); 696 | }); 697 | }); 698 | 699 | describe('ceil(x)', function () { 700 | it('rounds up to the nearest integer', function () { 701 | assert.strictEqual(parser.evaluate('ceil 0'), 0); 702 | assert.strictEqual(parser.evaluate('ceil 0.5'), 1); 703 | assert.strictEqual(parser.evaluate('ceil -0.5'), 0); 704 | assert.strictEqual(parser.evaluate('ceil 1'), 1); 705 | assert.strictEqual(parser.evaluate('ceil -1'), -1); 706 | assert.strictEqual(parser.evaluate('ceil 1.000001'), 2); 707 | assert.strictEqual(parser.evaluate('ceil -1.000001'), -1); 708 | assert.strictEqual(parser.evaluate('ceil 2.999'), 3); 709 | assert.strictEqual(parser.evaluate('ceil -2.999'), -2); 710 | assert.strictEqual(parser.evaluate('ceil 123.5'), 124); 711 | assert.strictEqual(parser.evaluate('ceil -123.5'), -123); 712 | assert.strictEqual(parser.evaluate('ceil(1/0)'), Infinity); 713 | assert.strictEqual(parser.evaluate('ceil(-1/0)'), -Infinity); 714 | }); 715 | }); 716 | 717 | describe('floor(x)', function () { 718 | it('rounds down to the nearest integer', function () { 719 | assert.strictEqual(parser.evaluate('floor 0'), 0); 720 | assert.strictEqual(parser.evaluate('floor 0.5'), 0); 721 | assert.strictEqual(parser.evaluate('floor -0.5'), -1); 722 | assert.strictEqual(parser.evaluate('floor 1'), 1); 723 | assert.strictEqual(parser.evaluate('floor -1'), -1); 724 | assert.strictEqual(parser.evaluate('floor 1.000001'), 1); 725 | assert.strictEqual(parser.evaluate('floor -1.000001'), -2); 726 | assert.strictEqual(parser.evaluate('floor 2.999'), 2); 727 | assert.strictEqual(parser.evaluate('floor -2.999'), -3); 728 | assert.strictEqual(parser.evaluate('floor 123.5'), 123); 729 | assert.strictEqual(parser.evaluate('floor -123.5'), -124); 730 | assert.strictEqual(parser.evaluate('floor(1/0)'), Infinity); 731 | assert.strictEqual(parser.evaluate('floor(-1/0)'), -Infinity); 732 | }); 733 | }); 734 | 735 | describe('round(x)', function () { 736 | it('rounds to the nearest integer', function () { 737 | assert.strictEqual(parser.evaluate('round 0'), 0); 738 | assert.strictEqual(parser.evaluate('round 0.4999'), 0); 739 | assert.strictEqual(parser.evaluate('round -0.4999'), 0); 740 | assert.strictEqual(parser.evaluate('round 0.5'), 1); 741 | assert.strictEqual(parser.evaluate('round -0.5'), 0); 742 | assert.strictEqual(parser.evaluate('round 0.5001'), 1); 743 | assert.strictEqual(parser.evaluate('round -0.5001'), -1); 744 | assert.strictEqual(parser.evaluate('round 1'), 1); 745 | assert.strictEqual(parser.evaluate('round -1'), -1); 746 | assert.strictEqual(parser.evaluate('round 1.000001'), 1); 747 | assert.strictEqual(parser.evaluate('round -1.000001'), -1); 748 | assert.strictEqual(parser.evaluate('round 1.5'), 2); 749 | assert.strictEqual(parser.evaluate('round -1.5'), -1); 750 | assert.strictEqual(parser.evaluate('round 2.999'), 3); 751 | assert.strictEqual(parser.evaluate('round -2.999'), -3); 752 | assert.strictEqual(parser.evaluate('round 2.5'), 3); 753 | assert.strictEqual(parser.evaluate('round -2.5'), -2); 754 | assert.strictEqual(parser.evaluate('round 123.5'), 124); 755 | assert.strictEqual(parser.evaluate('round -123.5'), -123); 756 | assert.strictEqual(parser.evaluate('round(1/0)'), Infinity); 757 | assert.strictEqual(parser.evaluate('round(-1/0)'), -Infinity); 758 | }); 759 | }); 760 | 761 | describe('trunc(x)', function () { 762 | it('rounds toward zero', function () { 763 | assert.strictEqual(parser.evaluate('trunc 0'), 0); 764 | assert.strictEqual(parser.evaluate('trunc 0.4999'), 0); 765 | assert.strictEqual(parser.evaluate('trunc -0.4999'), 0); 766 | assert.strictEqual(parser.evaluate('trunc 0.5'), 0); 767 | assert.strictEqual(parser.evaluate('trunc -0.5'), 0); 768 | assert.strictEqual(parser.evaluate('trunc 0.5001'), 0); 769 | assert.strictEqual(parser.evaluate('trunc -0.5001'), 0); 770 | assert.strictEqual(parser.evaluate('trunc 1'), 1); 771 | assert.strictEqual(parser.evaluate('trunc -1'), -1); 772 | assert.strictEqual(parser.evaluate('trunc 1.000001'), 1); 773 | assert.strictEqual(parser.evaluate('trunc -1.000001'), -1); 774 | assert.strictEqual(parser.evaluate('trunc 1.5'), 1); 775 | assert.strictEqual(parser.evaluate('trunc -1.5'), -1); 776 | assert.strictEqual(parser.evaluate('trunc 2.999'), 2); 777 | assert.strictEqual(parser.evaluate('trunc -2.999'), -2); 778 | assert.strictEqual(parser.evaluate('trunc 2.5'), 2); 779 | assert.strictEqual(parser.evaluate('trunc -2.5'), -2); 780 | assert.strictEqual(parser.evaluate('trunc 123.5'), 123); 781 | assert.strictEqual(parser.evaluate('trunc -123.5'), -123); 782 | assert.strictEqual(parser.evaluate('trunc(1/0)'), Infinity); 783 | assert.strictEqual(parser.evaluate('trunc(-1/0)'), -Infinity); 784 | }); 785 | }); 786 | 787 | describe('exp(x)', function () { 788 | it('rounds to the nearest integer', function () { 789 | var delta = 1e-15; 790 | assert.strictEqual(parser.evaluate('exp 0'), 1); 791 | assertCloseTo(parser.evaluate('exp 0.5'), 1.6487212707001282, delta); 792 | assertCloseTo(parser.evaluate('exp -0.5'), 0.6065306597126334, delta); 793 | assertCloseTo(parser.evaluate('exp 1'), 2.718281828459045, delta); 794 | assertCloseTo(parser.evaluate('exp -1'), 0.36787944117144233, delta); 795 | assertCloseTo(parser.evaluate('exp 1.5'), 4.4816890703380645, delta); 796 | assertCloseTo(parser.evaluate('exp -1.5'), 0.22313016014842982, delta); 797 | assertCloseTo(parser.evaluate('exp 2'), 7.3890560989306495, delta); 798 | assertCloseTo(parser.evaluate('exp -2'), 0.1353352832366127, delta); 799 | assertCloseTo(parser.evaluate('exp 2.5'), 12.182493960703471, delta * 10); 800 | assertCloseTo(parser.evaluate('exp -2.5'), 0.0820849986238988, delta); 801 | assertCloseTo(parser.evaluate('exp 3'), 20.085536923187668, delta); 802 | assertCloseTo(parser.evaluate('exp 4'), 54.59815003314423, delta * 10); 803 | assertCloseTo(parser.evaluate('exp 10'), 22026.46579480671, delta * 10000); 804 | assertCloseTo(parser.evaluate('exp -10'), 0.00004539992976248486, delta); 805 | assert.strictEqual(parser.evaluate('exp(1/0)'), Infinity); 806 | assert.strictEqual(parser.evaluate('exp(-1/0)'), 0); 807 | }); 808 | }); 809 | 810 | describe('-x', function () { 811 | it('has the correct precedence', function () { 812 | assert.strictEqual(parser.parse('-2^3').toString(), '(-(2 ^ 3))'); 813 | assert.strictEqual(parser.parse('-(2)^3').toString(), '(-(2 ^ 3))'); 814 | assert.strictEqual(parser.parse('-2 * 3').toString(), '((-2) * 3)'); 815 | assert.strictEqual(parser.parse('-2 + 3').toString(), '((-2) + 3)'); 816 | assert.strictEqual(parser.parse('- - 1').toString(), '(-(-1))'); 817 | }); 818 | 819 | it('negates its argument', function () { 820 | assert.strictEqual(parser.evaluate('-0'), 0); 821 | assert.strictEqual(parser.evaluate('-0.5'), -0.5); 822 | assert.strictEqual(parser.evaluate('-1'), -1); 823 | assert.strictEqual(parser.evaluate('-123'), -123); 824 | assert.strictEqual(parser.evaluate('-(-1)'), 1); 825 | }); 826 | 827 | it('converts its argument to a number', function () { 828 | assert.strictEqual(parser.evaluate('-"123"'), -123); 829 | }); 830 | }); 831 | 832 | describe('+x', function () { 833 | it('has the correct precedence', function () { 834 | assert.strictEqual(parser.parse('+2^3').toString(), '(+(2 ^ 3))'); 835 | assert.strictEqual(parser.parse('+(2)^3').toString(), '(+(2 ^ 3))'); 836 | assert.strictEqual(parser.parse('+2 * 3').toString(), '((+2) * 3)'); 837 | assert.strictEqual(parser.parse('+2 + 3').toString(), '((+2) + 3)'); 838 | assert.strictEqual(parser.parse('+ + 1').toString(), '(+(+1))'); 839 | }); 840 | 841 | it('returns its argument', function () { 842 | assert.strictEqual(parser.evaluate('+0'), 0); 843 | assert.strictEqual(parser.evaluate('+0.5'), 0.5); 844 | assert.strictEqual(parser.evaluate('+1'), 1); 845 | assert.strictEqual(parser.evaluate('+123'), 123); 846 | assert.strictEqual(parser.evaluate('+(+1)'), 1); 847 | }); 848 | 849 | it('converts its argument to a number', function () { 850 | assert.strictEqual(parser.evaluate('+"123"'), 123); 851 | }); 852 | }); 853 | 854 | describe('x!', function () { 855 | it('has the correct precedence', function () { 856 | assert.strictEqual(parser.parse('2^3!').toString(), '(2 ^ (3!))'); 857 | assert.strictEqual(parser.parse('-5!').toString(), '(-(5!))'); 858 | assert.strictEqual(parser.parse('4!^3').toString(), '((4!) ^ 3)'); 859 | assert.strictEqual(parser.parse('sqrt(4)!').toString(), '((sqrt 4)!)'); 860 | assert.strictEqual(parser.parse('sqrt 4!').toString(), '(sqrt (4!))'); 861 | assert.strictEqual(parser.parse('x!!').toString(), '((x!)!)'); 862 | }); 863 | 864 | it('returns exact answer for integers', function () { 865 | assert.strictEqual(parser.evaluate('(-10)!'), Infinity); 866 | assert.strictEqual(parser.evaluate('(-2)!'), Infinity); 867 | assert.strictEqual(parser.evaluate('(-1)!'), Infinity); 868 | assert.strictEqual(parser.evaluate('0!'), 1); 869 | assert.strictEqual(parser.evaluate('1!'), 1); 870 | assert.strictEqual(parser.evaluate('2!'), 2); 871 | assert.strictEqual(parser.evaluate('3!'), 6); 872 | assert.strictEqual(parser.evaluate('4!'), 24); 873 | assert.strictEqual(parser.evaluate('5!'), 120); 874 | assert.strictEqual(parser.evaluate('6!'), 720); 875 | assert.strictEqual(parser.evaluate('7!'), 5040); 876 | assert.strictEqual(parser.evaluate('8!'), 40320); 877 | assert.strictEqual(parser.evaluate('9!'), 362880); 878 | assert.strictEqual(parser.evaluate('10!'), 3628800); 879 | assert.strictEqual(parser.evaluate('25!'), 1.5511210043330984e+25); 880 | assert.strictEqual(parser.evaluate('50!'), 3.0414093201713376e+64); 881 | assert.strictEqual(parser.evaluate('100!'), 9.332621544394418e+157); 882 | assert.strictEqual(parser.evaluate('170!'), 7.257415615308004e+306); 883 | assert.strictEqual(parser.evaluate('171!'), Infinity); 884 | }); 885 | 886 | it('returns approximation for fractions', function () { 887 | var delta = 1e-14; 888 | assertCloseTo(parser.evaluate('(-2.5)!'), 2.36327180120735, delta); 889 | assertCloseTo(parser.evaluate('(-1.5)!'), -3.54490770181103, delta); 890 | assertCloseTo(parser.evaluate('(-0.75)!'), 3.625609908221908, delta); 891 | assertCloseTo(parser.evaluate('(-0.5)!'), 1.772453850905516, delta); 892 | assertCloseTo(parser.evaluate('(-0.25)!'), 1.225416702465177, delta); 893 | assertCloseTo(parser.evaluate('0.25!'), 0.906402477055477, delta); 894 | assertCloseTo(parser.evaluate('0.5!'), 0.886226925452758, delta); 895 | assertCloseTo(parser.evaluate('0.75!'), 0.9190625268488832, delta); 896 | assertCloseTo(parser.evaluate('1.5!'), 1.329340388179137, delta); 897 | assertCloseTo(parser.evaluate('84.9!'), 1.8056411593417e128, 1e115); 898 | assertCloseTo(parser.evaluate('85.1!'), 4.395670640362208e128, 1e115); 899 | assertCloseTo(parser.evaluate('98.6!'), 1.483280675613632e155, 1e142); 900 | assert.strictEqual(parser.evaluate('171.35!'), Infinity); 901 | assert.strictEqual(parser.evaluate('172.5!'), Infinity); 902 | }); 903 | 904 | it('handles NaN and infinity correctly', function () { 905 | assert.ok(isNaN(parser.evaluate('(0/0)!'))); 906 | assert.strictEqual(parser.evaluate('(1/0)!'), Infinity); 907 | assert.ok(isNaN(parser.evaluate('(-1/0)!'))); 908 | }); 909 | }); 910 | 911 | describe('[] operator', function () { 912 | it('a[0]', function () { 913 | assert.strictEqual(Parser.evaluate('a[0]', { a: [ 4, 3, 2, 1 ] }), 4); 914 | }); 915 | 916 | it('a[0.1]', function () { 917 | assert.strictEqual(Parser.evaluate('a[0.1]', { a: [ 4, 3, 2, 1 ] }), 4); 918 | }); 919 | 920 | it('a[3]', function () { 921 | assert.strictEqual(Parser.evaluate('a[3]', { a: [ 4, 3, 2, 1 ] }), 1); 922 | }); 923 | 924 | it('a[3 - 2]', function () { 925 | assert.strictEqual(Parser.evaluate('a[3 - 2]', { a: [ 4, 3, 2, 1 ] }), 3); 926 | }); 927 | 928 | it('a["foo"]', function () { 929 | assert.strictEqual(Parser.evaluate('a["foo"]', { a: { foo: 'bar' } }), undefined); 930 | }); 931 | 932 | it('a[2]^3', function () { 933 | assert.strictEqual(Parser.evaluate('a[2]^3', { a: [ 1, 2, 3, 4 ] }), 27); 934 | }); 935 | }); 936 | 937 | describe('sign(x)', function () { 938 | it('returns the sign of x', function () { 939 | assert.strictEqual(parser.evaluate('sign 0'), 0); 940 | assert.strictEqual(parser.evaluate('sign 1'), 1); 941 | assert.strictEqual(parser.evaluate('sign -1'), -1); 942 | assert.strictEqual(parser.evaluate('sign 2'), 1); 943 | assert.strictEqual(parser.evaluate('sign -2'), -1); 944 | assert.strictEqual(parser.evaluate('sign 0.001'), 1); 945 | assert.strictEqual(parser.evaluate('sign -0.001'), -1); 946 | 947 | assert.strictEqual(parser.parse('sign -0.001').simplify().toString(), '(-1)'); 948 | 949 | assert.strictEqual(parser.parse('sign x').toJSFunction('x')(0), 0); 950 | assert.strictEqual(parser.parse('sign x').toJSFunction('x')(2), 1); 951 | assert.strictEqual(parser.parse('sign x').toJSFunction('x')(-2), -1); 952 | }); 953 | }); 954 | 955 | describe('cbrt(x)', function () { 956 | it('returns the cube root of x', function () { 957 | var delta = 1e-15; 958 | 959 | assert.ok(isNaN(parser.evaluate('cbrt(0/0)'))); 960 | assert.strictEqual(parser.evaluate('cbrt -1'), -1); 961 | assert.strictEqual(parser.evaluate('cbrt 0'), 0); 962 | assert.strictEqual(parser.evaluate('cbrt(-1/0)'), -1/0); 963 | assert.strictEqual(parser.evaluate('cbrt 1'), 1); 964 | assert.strictEqual(parser.evaluate('cbrt(1/0)'), 1/0); 965 | assertCloseTo(parser.evaluate('cbrt 2'), 1.2599210498948732, delta); 966 | assertCloseTo(parser.evaluate('cbrt -2'), -1.2599210498948732, delta); 967 | assert.strictEqual(parser.evaluate('cbrt 8'), 2); 968 | assert.strictEqual(parser.evaluate('cbrt 27'), 3); 969 | assert.strictEqual(parser.evaluate('cbrt -8'), -2); 970 | assert.strictEqual(parser.evaluate('cbrt -27'), -3); 971 | 972 | assert.strictEqual(parser.parse('cbrt 8').simplify().toString(), '2'); 973 | 974 | assert.strictEqual(parser.parse('cbrt x').toJSFunction('x')(27), 3); 975 | }); 976 | }); 977 | 978 | describe('expm1(x)', function () { 979 | it('returns e^x - 1', function () { 980 | var delta = 1e-15; 981 | 982 | assert.ok(isNaN(parser.evaluate('expm1(0/0)'))); 983 | assertCloseTo(parser.evaluate('expm1 -1'), -0.6321205588285577, delta); 984 | assert.strictEqual(parser.evaluate('expm1 0'), 0); 985 | assertCloseTo(parser.evaluate('expm1 1'), 1.718281828459045, delta); 986 | assertCloseTo(parser.evaluate('expm1 2'), 6.38905609893065, delta); 987 | 988 | assert.ok(/^1.718281828459\d*$/.test(parser.parse('expm1 1').simplify().toString())); 989 | 990 | assertCloseTo(parser.parse('expm1 x').toJSFunction('x')(1), 1.718281828459045, delta); 991 | assertCloseTo(parser.parse('expm1 x').toJSFunction('x')(2), 6.38905609893065, delta); 992 | }); 993 | }); 994 | 995 | describe('log1p(x)', function () { 996 | it('returns e^x - 1', function () { 997 | var delta = 1e-15; 998 | 999 | assert.ok(isNaN(parser.evaluate('log1p(0/0)'))); 1000 | assert.strictEqual(parser.evaluate('log1p -1'), -1/0); 1001 | assert.strictEqual(parser.evaluate('log1p 0'), 0); 1002 | assertCloseTo(parser.evaluate('log1p 1'), 0.6931471805599453, delta); 1003 | assert.ok(isNaN(parser.evaluate('log1p -2'))); 1004 | assertCloseTo(Parser.evaluate('log1p 9'), 2.302585092994046, delta); 1005 | 1006 | assertCloseTo(parser.parse('log1p x').toJSFunction('x')(1), 0.6931471805599453, delta); 1007 | assertCloseTo(parser.parse('log1p x').toJSFunction('x')(9), 2.302585092994046, delta); 1008 | }); 1009 | }); 1010 | 1011 | describe('log2(x)', function () { 1012 | it('returns the base 2 log of x', function () { 1013 | var delta = 1e-15; 1014 | 1015 | assert.ok(isNaN(parser.evaluate('log2(0/0)'))); 1016 | assert.ok(isNaN(parser.evaluate('log2 -1'))); 1017 | assert.strictEqual(parser.evaluate('log2 0'), -1/0); 1018 | assert.strictEqual(Parser.evaluate('log2 1'), 0); 1019 | assert.strictEqual(Parser.evaluate('log2 2'), 1); 1020 | assert.strictEqual(Parser.evaluate('log2 3'), 1.584962500721156); 1021 | assert.strictEqual(Parser.evaluate('log2 4'), 2); 1022 | assert.strictEqual(Parser.evaluate('log2 8'), 3); 1023 | assert.strictEqual(Parser.evaluate('log2 1024'), 10); 1024 | 1025 | assert.strictEqual(parser.parse('log2 x').toJSFunction('x')(4), 2); 1026 | assertCloseTo(parser.parse('log2 x').toJSFunction('x')(3), 1.584962500721156, delta); 1027 | }); 1028 | }); 1029 | }); 1030 | --------------------------------------------------------------------------------