├── .gitignore ├── LICENSE.txt ├── README.md ├── README_CN.md ├── package.json ├── parser.js └── test └── parserSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JavaScript Expression Evaluator 2 | =============================== 3 | 4 | This project comes from https://github.com/silentmatt/js-expression-eval 5 | 6 | I rewrote the base evaluator of [js-expression-eval](https://github.com/silentmatt/js-expression-eval) to make it faster and scalable. 7 | 8 | Description 9 | ----------- 10 | 11 | This library is a modified version of Raphael Graf’s ActionScript Expression 12 | Parser. When I wrote the JavaScript Function Plotter, I wanted a better 13 | alternative to using JavaScript’s eval function. There’s no security risk 14 | currently, because you can only run code in your own browser, but it’s not as 15 | convenient for math (`Math.pow(2, x)` instead of `2^x`, etc.). 16 | 17 | Documentation (incomplete, of course) 18 | ------------------------------------- 19 | 20 | [中文版](README_CN.md) 21 | 22 | ### Quick start ### 23 | 24 | **Installation** 25 | 26 | ```bash 27 | npm install js-expression 28 | ``` 29 | 30 | **Usage** 31 | 32 | ```javascript 33 | var Parser = require('js-expression').Parser; 34 | 35 | function Complex(r, i){ 36 | this.r = r; 37 | this.i = i || 0; 38 | } 39 | 40 | Complex.prototype.toString = function(){ 41 | return this.r + '+' + this.i + 'i'; 42 | } 43 | 44 | var parser = new Parser(); 45 | 46 | parser.overload('+', Complex, function(a, b){ 47 | return new Complex(a.r + b.r, a.i + b.i); 48 | }); 49 | 50 | var c = parser.parse("a + b + 1"); 51 | var a = new Complex(1, 2); 52 | var b = new Complex(3, 4); 53 | 54 | //Complex { r: 5, i: 6 } 55 | console.log(c.evaluate({a:a, b:b})); 56 | ``` 57 | 58 | ### Parser ### 59 | 60 | Parser is the main class in the library. It has “static” methods for parsing 61 | and evaluating expressions. 62 | 63 | **Parser()** 64 | 65 | Constructor. In most cases, you don’t need this. Eventually, I’ll get around to 66 | documenting why you would want to, but for now, you can figure it 67 | out by reading the source ;-). 68 | 69 | **parse({expression: string})** 70 | 71 | Convert a mathematical expression into an Expression object. 72 | 73 | **evaluate({expression: string} [, {variables: object}])** 74 | 75 | Parse and immediately evaluate an expression using the values/functions from 76 | the {variables} object. 77 | 78 | Parser.evaluate(expr, vars) is equivalent to calling 79 | Parser.parse(expr).evaluate(vars). In fact, that’s exactly what it does. 80 | 81 | **addOperator({operator: string}, {priority: number}, {handler: function})** 82 | 83 | Add a new operator to evaluate an expression. 84 | 85 | ```javascript 86 | var parser = new Parser(); 87 | 88 | function Vector(x, y){ 89 | this.x = x; 90 | this.y = y; 91 | } 92 | 93 | //vector cross 94 | parser.addOperator('**', 3, function(a,b){ 95 | return new Vector(a.x * b.y, -b.x * a.y); 96 | }); 97 | 98 | var expr = parser.parse("a ** b"); 99 | 100 | //Vector { x: 4, y: -6 } 101 | console.log(expr.evaluate({ 102 | a: new Vector(1, 2), 103 | b: new Vector(3, 4) 104 | })); 105 | ``` 106 | 107 | **addFunction({name: string}, {handler: function}[, {can_simplify: boolean} = true])** 108 | 109 | Add a new function to evaluate an expression. 110 | 111 | ```javascript 112 | var parser = new Parser(); 113 | 114 | parser.addFunction('time', function(){ 115 | return Date.now(); 116 | },false); 117 | 118 | 119 | var expr = parser.parse("'abc?t='+time()"); 120 | 121 | console.log(expr.evaluate()); 122 | 123 | parser.addFunction('xor', function(a, b){ 124 | return a ^ b; 125 | }); 126 | 127 | var expr = parser.parse("xor(5, 7) + x + 1"); 128 | 129 | //((2+x)+1) 130 | console.log(expr.simplify().toString()); 131 | ``` 132 | 133 | **suffix operator** 134 | 135 | You can add an operator with a prefix `~` to make it be a suffix operator. 136 | 137 | ```javascript 138 | var parser = new Parser(); 139 | 140 | parser.addOperator('~%', 4, function(a){ 141 | return a / 100; 142 | }); 143 | 144 | var expr1 = parser.parse("((300% % 2)*10)!"); 145 | 146 | //3628800 147 | console.log(expr1.evaluate()); 148 | ``` 149 | 150 | **overload({operator: string}, {Class: constructor}, {handler: function})** 151 | 152 | Overload an operator for a new datatype. 153 | 154 | ```javascript 155 | var parser = new Parser(); 156 | 157 | function Vector(x, y){ 158 | this.x = x; 159 | this.y = y; 160 | } 161 | 162 | //vector cross 163 | parser.addOperator('**', 3, function(a,b){ 164 | return new Vector(a.x * b.y, -b.x * a.y); 165 | }); 166 | 167 | //vector add 168 | parser.overload('+', Vector, function(a, b){ 169 | return new Vector(a.x + b.x, a.y + b.y); 170 | }); 171 | 172 | var expr = parser.parse("a ** b + c"); 173 | 174 | console.log(expr.toString()); //((a**b)+c) 175 | console.log(expr.evaluate({ //Vector { x: 9, y: -7 } 176 | a: new Vector(1, 2), 177 | b: new Vector(3, 4), 178 | c: new Vector(5, -1), 179 | })); 180 | ``` 181 | 182 | Another example: 183 | 184 | ```javascript 185 | var parser = new Parser(); 186 | 187 | parser.overload('+', Array, function(a, b){ 188 | return a.concat(b); 189 | }); 190 | 191 | var expr3 = parser.parse("(1,2,3) + (4,5,6)"); 192 | 193 | //got [1,2,3,4,5,6] 194 | console.log(expr3.evaluate()); 195 | ``` 196 | 197 | ### Parser.Expression ### 198 | 199 | Parser.parse returns an Expression object. Expression objects are similar to 200 | JavaScript functions, i.e. they can be “called” with variables bound to 201 | passed-in values. In fact, they can even be converted into JavaScript 202 | functions. 203 | 204 | **evaluate([{variables: object}])** 205 | 206 | Evaluate an expression, with variables bound to the values in {variables}. Each 207 | unbound variable in the expression is bound to the corresponding member of the 208 | {variables} object. If there are unbound variables, evaluate will throw an 209 | exception. 210 | 211 | ```javascript 212 | var expr = Parser.parse("2 ^ x"); 213 | 214 | //8 215 | expr.evaluate({ x: 3 }); 216 | ``` 217 | 218 | **substitute({variable: string}, {expr: Expression, string, or number})** 219 | 220 | Create a new expression with the specified variable replaced with another 221 | expression (essentially, function composition). 222 | 223 | ```javascript 224 | var expr = Parser.parse("2 * x + 1"); 225 | //((2*x)+1) 226 | 227 | expr.substitute("x", "4 * x"); 228 | //((2*(4*x))+1) 229 | 230 | expr2.evaluate({ x: 3}); 231 | //25 232 | ``` 233 | 234 | **simplify({variables: object})** 235 | 236 | Simplify constant sub-expressions and replace 237 | variable references with literal values. This is basically a partial 238 | evaluation, that does as much of the calcuation as it can with the provided 239 | variables. Function calls are not evaluated (except the built-in operator 240 | functions), since they may not be deterministic. 241 | 242 | Simplify is pretty simple (see what I did there?). It doesn’t know that 243 | addition and multiplication are associative, so `((2*(4*x))+1)` from the 244 | previous example cannot be simplified unless you provide a value for x. 245 | `2*4*x+1` can however, because it’s parsed as `(((2*4)*x)+1)`, so the `(2*4)` 246 | sub-expression will be replaced with “8″, resulting in `((8*x)+1)`. 247 | 248 | ```javascript 249 | var expr = Parser.parse("x * (y * atan(1))").simplify({ y: 4 }); 250 | //(x*3.141592653589793) 251 | 252 | var expr.evaluate({ x: 2 }); 253 | //6.283185307179586 254 | ``` 255 | 256 | **simplify_exclude_functions** 257 | 258 | Some of the functions are not the pure functions. It means you may get a different value during each call, such as `random`. These functions cannot be simplified. 259 | 260 | ```javascript 261 | var expr = Parser.parse("1 + random()").simplify(); 262 | //(1+random()) 263 | ``` 264 | 265 | **variables([{include_functions: boolean}])** 266 | 267 | ```javascript 268 | //Get an array of the unbound variables in the expression. 269 | 270 | var expr = Parser.parse("x * (y * atan(1))"); 271 | //(x*(y*atan(1))) 272 | 273 | expr.variables(); 274 | //x,y 275 | 276 | expr.variables(true); 277 | //x,y,atan 278 | 279 | expr.simplify({ y: 4 }).variables(); 280 | //x 281 | ``` 282 | 283 | **toString()** 284 | 285 | Convert the expression to a string. toString() surrounds every sub-expression 286 | with parentheses (except literal values, variables, and function calls), so 287 | it’s useful for debugging precidence errors. 288 | 289 | **toJSFunction({parameters: Array} [, {variables: object}])** 290 | 291 | Convert an Expression object into a callable JavaScript function. You need to 292 | provide an array of parameter names that should normally be expr.variables(). 293 | Any unbound-variables will get their values from the global scope. 294 | 295 | toJSFunction works by simplifying the Expression (with {variables}, if 296 | provided), converting it to a string, and passing the string to the Function 297 | constructor (with some of its own code to bring built-in functions and 298 | constants into scope and return the result of the expression). 299 | 300 | ```javascript 301 | var expr = Parser.parse("x ^ 2 + y ^ 2 + 1"); 302 | var func1 = expr.toJSFunction(['x', 'y']); 303 | var func2 = expr.toJSFunction(['x'], {y: 2}); 304 | 305 | func1(1, 1); 306 | //3 307 | 308 | func2(2); 309 | //9 310 | ``` 311 | 312 | ### Expression Syntax ### 313 | 314 | The parser accepts a pretty basic grammar. Operators have the normal precidence 315 | — f(x,y,z) (function calls), ^ (exponentiation), *, /, and % (multiplication, 316 | division, and remainder), and finally +, -, and || (addition, subtraction, and 317 | string concatenation) — and bind from left to right (yes, even exponentiation… 318 | it’s simpler that way). 319 | 320 | Inside the first argument of the cond function can be used these operators to compare expressions: 321 | == Equal 322 | != Not equal 323 | > Greater than 324 | >= Greater or equal than 325 | < Less than 326 | <= Less or equal than 327 | and Logical AND operator 328 | or Logical OR operator 329 | 330 | Example of cond function: `cond(1 and 2 <= 4, 2, 0) + 2` = 4 331 | 332 | Function operators 333 | 334 | The parser has several built-in “functions” that are actually operators. The 335 | only difference from an outside point of view, is that they cannot be called 336 | with multiple arguments and they are evaluated by the simplify method if their 337 | arguments are constant. 338 | 339 | Function Description 340 | sin(x) Sine of x (x is in radians) 341 | cos(x) Cosine of x (x is in radians) 342 | tan(x) Tangent of x (x is… well, you know) 343 | asin(x) Arc sine of x (in radians) 344 | acos(x) Arc cosine of x (in radians) 345 | atan(x) Arc tangent of x (in radians) 346 | sinh(x) Hyperbolic sine of x (x is in radians) 347 | cosh(x) Hyperbolic cosine of x (x is in radians) 348 | tanh(x) Hyperbolic tangent of x (x is… well, you know) 349 | asinh(x) Hyperbolic arc sine of x (in radians) 350 | acosh(x) Hyperbolic arc cosine of x (in radians) 351 | atanh(x) Hyperbolic arc tangent of x (in radians) 352 | sqrt(x) Square root of x. Result is NaN (Not a Number) if x is negative. 353 | log(x) Natural logarithm of x (not base-10). It’s log instead of ln because that’s what JavaScript calls it. 354 | abs(x) Absolute value (magnatude) of x 355 | ceil(x) Ceiling of x — the smallest integer that’s >= x. 356 | floor(x) Floor of x — the largest integer that’s <= x. 357 | round(x) X, rounded to the nearest integer, using “gradeschool rounding”. 358 | trunc(x) Integral part of a X, looks like floor(x) unless for negative number. 359 | exp(x) ex (exponential/antilogarithm function with base e) Pre-defined functions 360 | 361 | Besides the “operator” functions, there are several pre-defined functions. You 362 | can provide your own, by binding variables to normal JavaScript functions. 363 | These are not evaluated by simplify. 364 | 365 | Function Description 366 | random(n) Get a random number in the range [0, n). If n is zero, or not provided, it defaults to 1. 367 | fac(n) n! (factorial of n: “n * (n-1) * (n-2) * … * 2 * 1″) 368 | min(a,b,…) Get the smallest (“minimum”) number in the list 369 | max(a,b,…) Get the largest (“maximum”) number in the list 370 | pyt(a, b) Pythagorean function, i.e. the c in “c2 = a2 + b2“ 371 | pow(x, y) xy. This is exactly the same as “x^y”. It’s just provided since it’s in the Math object from JavaScript 372 | atan2(y, x) Arc tangent of x/y. i.e. the angle between (0, 0) and (x, y) in radians. 373 | hypot(a,b) The square root of the sum of squares of its arguments. 374 | cond(c, a, b) The condition function where c is condition, a is result if c is true, b is result if c is false 375 | 376 | ### Tests ### 377 | 378 | To run tests, you need: 379 | 380 | 1. [Install NodeJS](https://github.com/nodejs/node) 381 | 2. Install Mocha `npm install -g mocha` 382 | 3. Install Chai `npm install chai` 383 | 4. Execute `mocha` 384 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # JavaScript 高级表达式运算解析器 2 | =============================== 3 | 4 | 这个项目来源于 https://github.com/silentmatt/js-expression-eval 5 | 6 | 我重写了 [js-expression-eval](https://github.com/silentmatt/js-expression-eval) 的底层,以获得非常高的性能和很好的扩展性。 7 | 8 | ## 支持以下特性: 9 | 10 | - 自定义运算符 11 | - 单目、双目、后缀运算符 12 | - 运算符重载 13 | - 公式化简 14 | - 公式迭代 15 | - 将公式转换成JavaScript原生函数 16 | 17 | ## 使用帮助 18 | 19 | ### 安装 20 | 21 | ```bash 22 | npm install js-expression 23 | ``` 24 | 25 | ### 使用 26 | 27 | ```javascript 28 | var Parser = require('js-expression').Parser; 29 | 30 | function Complex(r, i){ 31 | this.r = r; 32 | this.i = i || 0; 33 | } 34 | 35 | Complex.prototype.toString = function(){ 36 | return this.r + '+' + this.i + 'i'; 37 | } 38 | 39 | var parser = new Parser(); 40 | 41 | parser.overload('+', Complex, function(a, b){ 42 | return new Complex(a.r + b.r, a.i + b.i); 43 | }); 44 | 45 | var c = parser.parse("a + b + 1"); 46 | var a = new Complex(1, 2); 47 | var b = new Complex(3, 4); 48 | 49 | //Complex { r: 5, i: 6 } 50 | console.log(c.evaluate({a:a, b:b})); 51 | ``` 52 | 53 | ## Parser 对象 54 | 55 | **Parser( )** 56 | 57 | 构造函数,生成一个公式解析器对象。 58 | 59 | **parse({expression: string})** 60 | 61 | 解析字符串,得到一个具体的表达式。 62 | 63 | ```javascript 64 | var Parser = require('js-expression').Parser; 65 | 66 | var parser = new Parser(); 67 | var expr = parser.parse('x ^ 2 + y ^ 2'); 68 | console.log(expr.evalute({x: 3, y: 4})); 69 | ``` 70 | 71 | **static parse** 72 | 73 | parse 的静态版本。 74 | 75 | ```javascript 76 | var Parser = require('js-expression').Parser; 77 | 78 | var expr = Parser.parse('x ^ 2 + y ^ 2'); 79 | console.log(expr.evalute({x: 3, y: 4})); 80 | ``` 81 | 82 | **static evaluate({expression: string} [, {variables: object}])** 83 | 84 | 直接解析字符串得到公式并求值。 85 | 86 | ```javascript 87 | var Parser = require('js-expression').Parser; 88 | var result = Parser.evaluate('x ^ 2 + y ^ 2', {x:3, y:4}); 89 | ``` 90 | 91 | **addOperator({operator: string}, {priority: number}, {handler: function})** 92 | 93 | 增加一个运算符。 94 | 95 | ```javascript 96 | var parser = new Parser(); 97 | 98 | function Vector(x, y){ 99 | this.x = x; 100 | this.y = y; 101 | } 102 | 103 | //vector cross 104 | parser.addOperator('**', 3, function(a,b){ 105 | return new Vector(a.x * b.y, -b.x * a.y); 106 | }); 107 | 108 | var expr = parser.parse("a ** b"); 109 | 110 | //Vector { x: 4, y: -6 } 111 | console.log(expr.evaluate({ 112 | a: new Vector(1, 2), 113 | b: new Vector(3, 4) 114 | })); 115 | ``` 116 | 117 | **addFunction({name: string}, {handler: function}[, {can_simplify: boolean} = true])** 118 | 119 | 增加一个函数。 120 | 121 | ```javascript 122 | var parser = new Parser(); 123 | 124 | parser.addFunction('time', function(){ 125 | return Date.now(); 126 | },false); 127 | 128 | 129 | var expr = parser.parse("'abc?t='+time()"); 130 | 131 | console.log(expr.evaluate()); 132 | 133 | parser.addFunction('xor', function(a, b){ 134 | return a ^ b; 135 | }); 136 | 137 | var expr = parser.parse("xor(5, 7) + x + 1"); 138 | 139 | //((2+x)+1) 140 | console.log(expr.simplify().toString()); 141 | ``` 142 | 143 | **suffix operator** 144 | 145 | 后缀运算符:你可以在函数名前面加上`~`符号,表示定义的是一个“后缀运算符”。 146 | 147 | ```javascript 148 | var parser = new Parser(); 149 | 150 | parser.addOperator('~%', 4, function(a){ 151 | return a / 100; 152 | }); 153 | 154 | var expr1 = parser.parse("((300% % 2)*10)!"); 155 | 156 | //3628800 157 | console.log(expr1.evaluate()); 158 | ``` 159 | 160 | **overload({operator: string}, {Class: constructor}, {handler: function})** 161 | 162 | 针对特定的数据类型重载一个运算符。**注意**重载的运算符触发条件为任意一个参数的类型满足该重载函数所匹配的类型,并且会自动将所有的参数类型转换为重载函数匹配的类型。 163 | 164 | ```javascript 165 | var parser = new Parser(); 166 | 167 | function Vector(x, y){ 168 | this.x = x; 169 | this.y = y; 170 | } 171 | 172 | //vector cross 173 | parser.addOperator('**', 3, function(a,b){ 174 | return new Vector(a.x * b.y, -b.x * a.y); 175 | }); 176 | 177 | //vector add 178 | parser.overload('+', Vector, function(a, b){ 179 | return new Vector(a.x + b.x, a.y + b.y); 180 | }); 181 | 182 | var expr = parser.parse("a ** b + c"); 183 | 184 | console.log(expr.toString()); //((a**b)+c) 185 | console.log(expr.evaluate({ //Vector { x: 9, y: -7 } 186 | a: new Vector(1, 2), 187 | b: new Vector(3, 4), 188 | c: new Vector(5, -1), 189 | })); 190 | ``` 191 | 192 | ## Parser.Expression 对象 193 | 194 | Parser.parse 方法返回一个 Expression 对象。 195 | 196 | **evaluate([{variables: object}])** 197 | 198 | 用指定的变量值对表达式求值。 199 | 200 | ```javascript 201 | var expr = Parser.parse("2 ^ x"); 202 | 203 | //8 204 | expr.evaluate({ x: 3 }); 205 | ``` 206 | 207 | **substitute({variable: string}, {expr: Expression, string, or number})** 208 | 209 | 用变量表达式对当前表达式进行迭代。 210 | 211 | ```javascript 212 | var expr = Parser.parse("2 * x + 1"); 213 | //((2*x)+1) 214 | 215 | expr.substitute("x", "4 * x"); 216 | //((2*(4*x))+1) 217 | 218 | expr2.evaluate({ x: 3}); 219 | //25 220 | ``` 221 | 222 | **simplify({variables: object})** 223 | 224 | 对表达式进行化简。 225 | 226 | ```javascript 227 | var expr = Parser.parse("x * (y * atan(1))").simplify({ y: 4 }); 228 | //(x*3.141592653589793) 229 | 230 | var expr.evaluate({ x: 2 }); 231 | //6.283185307179586 232 | ``` 233 | 234 | **simplify_exclude_functions** 235 | 236 | 某些函数每次相同参数调用时返回值不一定相同(比如random),这类函数不能用`simplify`化简。我们可以在addFunction函数中设置最后一个参数为false来避免函数被`simplify`化简。默认`random`函数不可以化简。 237 | 238 | ```javascript 239 | var expr = Parser.parse("1 + random()").simplify(); 240 | //(1+random()) 241 | ``` 242 | 243 | **variables([{include_functions: boolean}])** 244 | 245 | ```javascript 246 | //Get an array of the unbound variables in the expression. 247 | 248 | var expr = Parser.parse("x * (y * atan(1))"); 249 | //(x*(y*atan(1))) 250 | 251 | expr.variables(); 252 | //x,y 253 | 254 | expr.variables(true); 255 | //x,y,atan 256 | 257 | expr.simplify({ y: 4 }).variables(); 258 | //x 259 | ``` 260 | 261 | **toString()** 262 | 263 | 将表达式转成字符串。 264 | 265 | **toJSFunction({parameters: Array} [, {variables: object}])** 266 | 267 | 将表达式转化为JavaScript函数。 268 | 269 | ```javascript 270 | var expr = Parser.parse("x ^ 2 + y ^ 2 + 1"); 271 | var func1 = expr.toJSFunction(['x', 'y']); 272 | var func2 = expr.toJSFunction(['x'], {y: 2}); 273 | 274 | func1(1, 1); 275 | //3 276 | 277 | func2(2); 278 | //9 279 | ``` 280 | 281 | ## 默认支持的运算符 282 | 283 | **单目运算符** 284 | 285 | "-": neg, 286 | "+": positive, 287 | 288 | **后缀运算符** 289 | 290 | "!": fac //阶乘 291 | 292 | **双目运算符** 293 | 294 | "+": add, 295 | "-": sub, 296 | "*": mul, 297 | "/": div, 298 | "%": mod, 299 | "^": Math.pow, 300 | "||": concat, 301 | "==": equal, 302 | "!=": notEqual, 303 | ">": greaterThan, 304 | "<": lessThan, 305 | ">=": greaterThanEqual, 306 | "<=": lessThanEqual, 307 | "and": andOperator, 308 | "or": orOperator 309 | 310 | **特殊运算符** 311 | 312 | ",": comma, 313 | 314 | **内置函数** 315 | 316 | "random": random, 317 | "fac": fac, 318 | "min": Math.min, 319 | "max": Math.max, 320 | "hypot": hypot, 321 | "pyt": hypot, // backward compat 322 | "pow": Math.pow, 323 | "atan2": Math.atan2, 324 | "cond": condition, 325 | 326 | "sin": Math.sin, 327 | "cos": Math.cos, 328 | "tan": Math.tan, 329 | "asin": Math.asin, 330 | "acos": Math.acos, 331 | "atan": Math.atan, 332 | "sinh": sinh, 333 | "cosh": cosh, 334 | "tanh": tanh, 335 | "asinh": asinh, 336 | "acosh": acosh, 337 | "atanh": atanh, 338 | "sqrt": Math.sqrt, 339 | "log": Math.log, 340 | "lg" : log10, 341 | "log10" : log10, 342 | "abs": Math.abs, 343 | "ceil": Math.ceil, 344 | "floor": Math.floor, 345 | "round": Math.round, 346 | "trunc": trunc, 347 | "exp": Math.exp 348 | 349 | ## 测试 350 | 351 | 执行测试用例: 352 | 353 | 1. [Install NodeJS](https://github.com/nodejs/node) 354 | 2. Install Mocha `npm install -g mocha` 355 | 3. Install Chai `npm install chai` 356 | 4. Execute `mocha` 357 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-expression", 3 | "version": "1.0.5", 4 | "description": "JavaScript Expression Evaluator", 5 | "main": "parser.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/akira-cn/js-expression.git" 15 | }, 16 | "keywords": [ 17 | "expression", 18 | "operator", 19 | "overload" 20 | ], 21 | "author": "akira.cn@gmail.com,email@matthewcrumley.com", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/akira-cn/js-expression/issues" 25 | }, 26 | "homepage": "https://github.com/akira-cn/js-expression#readme", 27 | "dependencies": { 28 | 29 | }, 30 | "devDependencies": { 31 | "Mocha": "^2.3.4", 32 | "Chai": "^3.4.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /parser.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 | var Parser = (function (scope) { 13 | function object(o) { 14 | function F() {} 15 | F.prototype = o; 16 | return new F(); 17 | } 18 | 19 | var TNUMBER = 0; 20 | var TOP1 = 1; 21 | var TOP2 = 2; 22 | var TVAR = 3; 23 | var TFUNCALL = 4; 24 | 25 | function Token(type_, index_, prio_, number_) { 26 | this.type_ = type_; 27 | this.index_ = index_ || 0; 28 | this.prio_ = prio_ || 0; 29 | this.number_ = (number_ !== undefined && number_ !== null) ? number_ : 0; 30 | this.toString = function () { 31 | switch (this.type_) { 32 | case TNUMBER: 33 | return this.number_; 34 | case TOP1: 35 | case TOP2: 36 | case TVAR: 37 | return this.index_; 38 | case TFUNCALL: 39 | return "CALL"; 40 | default: 41 | return "Invalid Token"; 42 | } 43 | }; 44 | } 45 | 46 | function Expression(tokens, ops1, ops2, functions, 47 | overload_ops1, overload_ops2, simplify_exclude_functions) { 48 | 49 | this.tokens = tokens; 50 | this.ops1 = ops1; 51 | this.ops2 = ops2; 52 | this.functions = functions; 53 | this.overload_ops1 = overload_ops1; 54 | this.overload_ops2 = overload_ops2; 55 | this.simplify_exclude_functions = simplify_exclude_functions; 56 | 57 | this.js_expr_str = this.toString('$_EXPR_OPS_$1', '$_EXPR_OPS_$2'); 58 | this.evaluate = this.toEvalFunction(); 59 | } 60 | 61 | // Based on http://www.json.org/json2.js 62 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 63 | escapable = /[\\\'\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 64 | meta = { // table of character substitutions 65 | '\b': '\\b', 66 | '\t': '\\t', 67 | '\n': '\\n', 68 | '\f': '\\f', 69 | '\r': '\\r', 70 | "'" : "\\'", 71 | '\\': '\\\\' 72 | }; 73 | 74 | function escapeValue(v) { 75 | if (typeof v === "string") { 76 | escapable.lastIndex = 0; 77 | return escapable.test(v) ? 78 | "'" + v.replace(escapable, function (a) { 79 | var c = meta[a]; 80 | return typeof c === 'string' ? c : 81 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 82 | }) + "'" : 83 | "'" + v + "'"; 84 | } 85 | return v; 86 | } 87 | 88 | Expression.prototype = { 89 | simplify: function (values) { 90 | values = values || {}; 91 | var nstack = []; 92 | var newexpression = []; 93 | var n1; 94 | var n2; 95 | var f; 96 | var L = this.tokens.length; 97 | var item; 98 | var i = 0; 99 | 100 | for (i = 0; i < L; i++) { 101 | item = this.tokens[i]; 102 | var type_ = item.type_; 103 | var index_ = item.index_; 104 | 105 | if (type_ === TNUMBER) { 106 | nstack.push(item); 107 | } 108 | else if (type_ === TVAR && 109 | (index_ in values || index_ in this.functions)) { 110 | if(index_ in this.functions){ 111 | var name = index_, func = values[name]; 112 | if(typeof func === 'function'){ 113 | this.functions[name] = func; 114 | } 115 | item = new Token(TVAR, name, 0, 0); 116 | }else if(typeof values[index_] === 'function'){ 117 | var func = values[index_]; 118 | this.functions[index_] = func; 119 | item = new Token(TVAR, index_, 0, 0); 120 | }else{ 121 | item = new Token(TNUMBER, 0, 0, values[index_]); 122 | } 123 | nstack.push(item); 124 | } 125 | else if (type_ === TOP2 && nstack.length > 1) { 126 | //comma expr cannot be simplified 127 | if(index_ !== ','){ 128 | n2 = nstack.pop(); 129 | n1 = nstack.pop(); 130 | f = this.ops2[index_]; 131 | item = new Token(TNUMBER, 0, 0, f(n1.number_, n2.number_)); 132 | nstack.push(item); 133 | }else{ 134 | n2 = nstack.pop(); 135 | n1 = nstack.pop(); 136 | f = this.ops2[index_]; 137 | var arr = append(n1.list_||n1.number_, n2.number_); 138 | item = new Token(TNUMBER, 0, 0, f(n1.number_, n2.number_)); 139 | item.list_ = arr; 140 | nstack.push(item); 141 | } 142 | } 143 | else if (type_ === TOP1 && nstack.length > 0) { 144 | n1 = nstack.pop(); 145 | f = this.ops1[index_]; 146 | item = new Token(TNUMBER, 0, 0, f(n1.number_)); 147 | nstack.push(item); 148 | }else if(type_ === TFUNCALL && nstack.length > 1){ 149 | n1 = nstack.pop(); 150 | f = nstack.pop(); 151 | if(this.simplify_exclude_functions.indexOf(f.index_) < 0){ 152 | var _f = this.functions[f.index_]; 153 | if(n1.type_ === TNUMBER && _f.apply && _f.call){ 154 | n1 = n1.list_ || n1.number_; 155 | if (Object.prototype.toString.call(n1) == "[object Array]") { 156 | item = new Token(TNUMBER, 0, 0, _f.apply(undefined, n1)); 157 | } 158 | else { 159 | item = new Token(TNUMBER, 0, 0, _f.call(undefined, n1)); 160 | } 161 | nstack.push(item); 162 | }else{ 163 | nstack.push(f, n1); 164 | } 165 | }else{ 166 | nstack.push(f, n1); 167 | newexpression.push.apply(newexpression, nstack); 168 | newexpression.push(item); 169 | nstack = []; 170 | } 171 | }else { 172 | newexpression.push.apply(newexpression, nstack); 173 | newexpression.push(item); 174 | nstack = []; 175 | } 176 | } 177 | newexpression.push.apply(newexpression, nstack); 178 | nstack = []; 179 | 180 | newexpression = newexpression.map(function(o){ 181 | if(o.list_){ 182 | delete o.list_; 183 | } 184 | return o; 185 | }); 186 | 187 | return new Expression( 188 | newexpression, 189 | this.ops1, 190 | this.ops2, 191 | this.functions, 192 | this.overload_ops1, 193 | this.overload_ops2, 194 | this.simplify_exclude_functions); 195 | }, 196 | 197 | substitute: function (variable, expr) { 198 | if (!(expr instanceof Expression)) { 199 | expr = new Parser().parse(String(expr)); 200 | } 201 | var newexpression = []; 202 | var L = this.tokens.length; 203 | var item; 204 | var i = 0; 205 | for (i = 0; i < L; i++) { 206 | item = this.tokens[i]; 207 | var type_ = item.type_; 208 | if (type_ === TVAR && item.index_ === variable) { 209 | for (var j = 0; j < expr.tokens.length; j++) { 210 | var expritem = expr.tokens[j]; 211 | var replitem = new Token(expritem.type_, expritem.index_, expritem.prio_, expritem.number_); 212 | newexpression.push(replitem); 213 | } 214 | } 215 | else { 216 | newexpression.push(item); 217 | } 218 | } 219 | 220 | var ret = new Expression( 221 | newexpression, 222 | this.ops1, 223 | this.ops2, 224 | this.functions, 225 | this.overload_ops1, 226 | this.overload_ops2, 227 | this.simplify_exclude_functions); 228 | 229 | return ret; 230 | }, 231 | 232 | toString: function (overload_1, overload_2) { 233 | function isBracketMatch(str){ 234 | str = str+""; 235 | if(str.charAt(0) !== '(') return false; 236 | var m = 1; 237 | for(var i = 1; i < str.length; i++){ 238 | var ch = str.charAt(i); 239 | if(ch === '(') m++; 240 | else if(ch === ')') m--; 241 | 242 | if(!m){ 243 | return i === str.length - 1; 244 | } 245 | } 246 | return false; 247 | } 248 | 249 | var nstack = []; 250 | var n1; 251 | var n2; 252 | var f; 253 | var L = this.tokens.length; 254 | var item; 255 | var i = 0; 256 | var comma_isclosed = true, comma_prio; 257 | 258 | for (i = 0; i < L; i++) { 259 | item = this.tokens[i]; 260 | var type_ = item.type_; 261 | if (type_ === TNUMBER) { 262 | nstack.push(escapeValue(item.number_)); 263 | } 264 | else if (type_ === TVAR) { 265 | nstack.push(item.index_); 266 | } 267 | else if (type_ === TOP2) { 268 | n2 = nstack.pop(); 269 | n1 = nstack.pop(); 270 | f = item.index_; 271 | if (overload_2 && (f in this.overload_ops2)) { 272 | comma_isclosed = true; 273 | if((n1+"").indexOf(",") >= 0 && !isBracketMatch(n1)){ 274 | n1 = "(" + n1 + ")"; 275 | } 276 | if((n2+"").indexOf(",") >= 0 && !isBracketMatch(n2)){ 277 | n2 = "(" + n2 + ")"; 278 | } 279 | nstack.push(overload_2 + "['" + f + "'](" + n1 + "," + n2 + ")"); 280 | }else{ 281 | if(f !== ','){ 282 | if(!comma_isclosed){ 283 | comma_isclosed = true; 284 | if(!isBracketMatch(n1)){ 285 | n1 = "(" + n1 + ")"; 286 | } 287 | nstack.push("(" + n1 + f + "(" + n2 + "))"); 288 | }else{ 289 | nstack.push("(" + n1 + f + n2 + ")"); 290 | } 291 | }else{ 292 | if(!comma_isclosed && comma_prio != null && 293 | item.prio_ != comma_prio){ 294 | if((n1+"").indexOf(",") >= 0){ 295 | n1 = "(" + n1 + ")"; 296 | } 297 | if((n2+"").indexOf(",") >= 0){ 298 | n2 = "(" + n2 + ")"; 299 | } 300 | } 301 | 302 | nstack.push(n1 + f + n2); 303 | comma_isclosed = false; 304 | comma_prio = item.prio_; 305 | } 306 | } 307 | } 308 | else if (type_ === TOP1) { 309 | n1 = nstack.pop(); 310 | f = item.index_; 311 | if (this.ops1[f]) { 312 | if(overload_1 && (f in this.overload_ops1)){ 313 | comma_isclosed = true; 314 | var prev = this.tokens[i - 1]; 315 | if(prev.prio_ - item.prio_ > 10){ 316 | n1 = "(" + n1 + ")"; 317 | } 318 | if(prev.index_ === ','){ 319 | n1 = "(" + n1 + ")"; 320 | } 321 | nstack.push(overload_1 + "['" + f + "'](" + n1 + ")"); 322 | }else{ 323 | if(!comma_isclosed){ 324 | comma_isclosed = true; 325 | nstack.push("(" + f + "(" + n1 + "))"); 326 | }else{ 327 | nstack.push("(" + f + n1 + ")"); 328 | } 329 | } 330 | } 331 | else { 332 | nstack.push(f + "(" + n1 + ")"); 333 | } 334 | } 335 | else if (type_ === TFUNCALL) { 336 | var prev = this.tokens[i - 1]; 337 | comma_isclosed = true; 338 | n1 = nstack.pop(); 339 | f = nstack.pop(); 340 | if(prev.prio_ - item.prio_ > 10){ 341 | n1 = "(" + n1 + ")"; 342 | } 343 | if(prev.index_ === ','){ 344 | n1 = "(" + n1 + ")"; 345 | } 346 | if(!isBracketMatch(n1)){ 347 | n1 = "(" + n1 + ")"; 348 | } 349 | nstack.push(f + n1); 350 | } 351 | else { 352 | throw new Error("invalid Expression"); 353 | } 354 | } 355 | if (nstack.length > 1) { 356 | throw new Error("invalid Expression (parity)"); 357 | } 358 | return nstack[0].toString(); 359 | }, 360 | 361 | variables: function (include_functions) { 362 | var L = this.tokens.length; 363 | var vars = []; 364 | for (var i = 0; i < L; i++) { 365 | var item = this.tokens[i]; 366 | if (item.type_ === TVAR 367 | && (include_functions || !(item.index_ in this.functions)) 368 | && (vars.indexOf(item.index_) == -1)) { 369 | vars.push(item.index_); 370 | } 371 | } 372 | 373 | return vars; 374 | }, 375 | 376 | toEvalFunction: function(){ 377 | var expr = this; 378 | var vars = expr.variables(true); 379 | var overload_str = 'overload_' + (Math.random() + '').slice(2); 380 | 381 | vars = vars.concat(overload_str + '1', overload_str + '2'); 382 | var code = expr.js_expr_str.replace(/\$_EXPR_OPS_\$/g, overload_str); 383 | var func = new Function(vars, "return " + code); 384 | 385 | return function(values){ 386 | values = values || {}; 387 | var params = []; 388 | 389 | for(var i = 0; i < vars.length - 2; i++){ 390 | var v, key = vars[i]; 391 | 392 | if(key in values){ 393 | v = values[key]; 394 | }else{ 395 | var f = expr.functions[key]; 396 | if(f){ 397 | v = f; 398 | } 399 | } 400 | 401 | params.push(v); 402 | } 403 | 404 | return func.apply(undefined, 405 | params.concat(expr.overload_ops1, expr.overload_ops2)); 406 | } 407 | }, 408 | 409 | toJSFunction: function(param_table, values){ 410 | var expr = this; 411 | if(values) expr = this.simplify(values); 412 | 413 | var vars = expr.variables(true); 414 | 415 | return function(){ 416 | var args = [].slice.call(arguments); 417 | var params = []; 418 | 419 | for(var i = 0; i < vars.length; i++){ 420 | var v, key = vars[i]; 421 | var idx = param_table.indexOf(key); 422 | if(idx >= 0){ 423 | v = args[idx]; 424 | }else{ 425 | var f = expr.functions[key]; 426 | if(f){ 427 | v = f; 428 | } 429 | } 430 | 431 | params.push(v); 432 | } 433 | 434 | var overload_str = 'overload_' + (Math.random() + '').slice(2); 435 | 436 | var f = new Function(vars.concat(overload_str + '1', overload_str + '2'), "return " + expr.js_expr_str.replace(/\$_EXPR_OPS_\$/g, overload_str)); 437 | return f.apply(undefined, 438 | params.concat(expr.overload_ops1, expr.overload_ops2)); 439 | } 440 | } 441 | }; 442 | 443 | function comma(){ 444 | return arguments[arguments.length - 1]; 445 | } 446 | function add(a, b) { 447 | return a + b; 448 | } 449 | function sub(a, b) { 450 | return a - b; 451 | } 452 | function mul(a, b) { 453 | return a * b; 454 | } 455 | function div(a, b) { 456 | return a / b; 457 | } 458 | function mod(a, b) { 459 | return a % b; 460 | } 461 | function concat(a, b) { 462 | return "" + a + b; 463 | } 464 | function equal(a, b) { 465 | return a == b; 466 | } 467 | function notEqual(a, b) { 468 | return a != b; 469 | } 470 | function greaterThan(a, b) { 471 | return a > b; 472 | } 473 | function lessThan(a, b) { 474 | return a < b; 475 | } 476 | function greaterThanEqual(a, b) { 477 | return a >= b; 478 | } 479 | function lessThanEqual(a, b) { 480 | return a <= b; 481 | } 482 | function andOperator(a, b) { 483 | return Boolean(a && b); 484 | } 485 | function orOperator(a, b) { 486 | return Boolean(a || b); 487 | } 488 | function sinh(a) { 489 | return Math.sinh ? Math.sinh(a) : ((Math.exp(a) - Math.exp(-a)) / 2); 490 | } 491 | function cosh(a) { 492 | return Math.cosh ? Math.cosh(a) : ((Math.exp(a) + Math.exp(-a)) / 2); 493 | } 494 | function tanh(a) { 495 | if (Math.tanh) return Math.tanh(a); 496 | if(a === Infinity) return 1; 497 | if(a === -Infinity) return -1; 498 | return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a)); 499 | } 500 | function asinh(a) { 501 | if (Math.asinh) return Math.asinh(a); 502 | if(a === -Infinity) return a; 503 | return Math.log(a + Math.sqrt(a * a + 1)); 504 | } 505 | function acosh(a) { 506 | return Math.acosh ? Math.acosh(a) : Math.log(a + Math.sqrt(a * a - 1)); 507 | } 508 | function atanh(a) { 509 | return Math.atanh ? Math.atanh(a) : (Math.log((1+a)/(1-a)) / 2); 510 | } 511 | function log10(a) { 512 | return Math.log(a) * Math.LOG10E; 513 | } 514 | function neg(a) { 515 | return -a; 516 | } 517 | function positive(a){ 518 | return a; 519 | } 520 | function trunc(a) { 521 | if(Math.trunc) return Math.trunc(a); 522 | else return a < 0 ? Math.ceil(a) : Math.floor(a); 523 | } 524 | function random(a) { 525 | return Math.random() * (a || 1); 526 | } 527 | function fac(a) { //a! 528 | a = 0 | a; 529 | if(a === 0) return 1; 530 | else if(a < 0) return NaN; 531 | else { 532 | var b = a; 533 | while (a > 1) { 534 | b = b * (--a); 535 | } 536 | return b; 537 | } 538 | } 539 | 540 | // TODO: use hypot that doesn't overflow 541 | function hypot() { 542 | if(Math.hypot) return Math.hypot.apply(this, arguments); 543 | var y = 0; 544 | var length = arguments.length; 545 | for (var i = 0; i < length; i++) { 546 | if (arguments[i] === Infinity || arguments[i] === -Infinity) { 547 | return Infinity; 548 | } 549 | y += arguments[i] * arguments[i]; 550 | } 551 | return Math.sqrt(y); 552 | } 553 | 554 | function condition(cond, yep, nope) { 555 | return cond ? yep : nope; 556 | } 557 | 558 | function append(a, b) { 559 | if (Object.prototype.toString.call(a) != "[object Array]") { 560 | return [a, b]; 561 | } 562 | a = a.slice(); 563 | a.push(b); 564 | return a; 565 | } 566 | 567 | function list(){ 568 | return [].slice.call(arguments); 569 | } 570 | 571 | function Parser() { 572 | this.success = false; 573 | this.errormsg = ""; 574 | this.expression = ""; 575 | 576 | this.pos = 0; 577 | 578 | this.tokennumber = 0; 579 | this.tokenprio = 0; 580 | this.tokenindex = 0; 581 | this.tmpprio = 0; 582 | 583 | this.ops1 = { 584 | "-": neg, 585 | "+": positive, 586 | "~!": fac, //Suffix operator: begin with ~ 587 | "~?": comma 588 | }; 589 | 590 | this.ops2 = { 591 | "+": add, 592 | "-": sub, 593 | "*": mul, 594 | "/": div, 595 | "%": mod, 596 | "^": Math.pow, 597 | ",": comma, 598 | "||": concat, 599 | "==": equal, 600 | "!=": notEqual, 601 | ">": greaterThan, 602 | "<": lessThan, 603 | ">=": greaterThanEqual, 604 | "<=": lessThanEqual, 605 | "and": andOperator, 606 | "or": orOperator 607 | }; 608 | 609 | this.overload_ops1 = { 610 | "~!": fac, 611 | "~?": comma 612 | }; 613 | 614 | this.overload_ops2 = { 615 | "^": Math.pow, 616 | "and": andOperator, 617 | "or": orOperator 618 | }; 619 | 620 | this.tokenprio_map1 = { 621 | "-": 4, 622 | "+": 4, 623 | "~!": 4, 624 | "~?": 6 625 | }; 626 | 627 | this.tokenprio_map2 = { 628 | "," : 0, 629 | "or": 0, 630 | "and": 0, 631 | "||": 1, 632 | "==": 1, 633 | "!=": 1, 634 | ">": 1, 635 | "<": 1, 636 | ">=": 1, 637 | "<=": 1, 638 | "+": 2, 639 | "-": 2, 640 | "*": 3, 641 | "/": 4, 642 | "%": 4, 643 | "^": 5 644 | }; 645 | 646 | this.functions = { 647 | "random": random, 648 | "fac": fac, 649 | "min": Math.min, 650 | "max": Math.max, 651 | "hypot": hypot, 652 | "pyt": hypot, // backward compat 653 | "pow": Math.pow, 654 | "atan2": Math.atan2, 655 | "cond": condition, 656 | 657 | "sin": Math.sin, 658 | "cos": Math.cos, 659 | "tan": Math.tan, 660 | "asin": Math.asin, 661 | "acos": Math.acos, 662 | "atan": Math.atan, 663 | "sinh": sinh, 664 | "cosh": cosh, 665 | "tanh": tanh, 666 | "asinh": asinh, 667 | "acosh": acosh, 668 | "atanh": atanh, 669 | "sqrt": Math.sqrt, 670 | "log": Math.log, 671 | "lg" : log10, 672 | "log10" : log10, 673 | "abs": Math.abs, 674 | "ceil": Math.ceil, 675 | "floor": Math.floor, 676 | "round": Math.round, 677 | "trunc": trunc, 678 | "exp": Math.exp 679 | }; 680 | 681 | this.simplify_exclude_functions = [ 682 | "random" 683 | ]; 684 | 685 | this.consts = { 686 | "E": Math.E, 687 | "PI": Math.PI 688 | }; 689 | } 690 | 691 | Parser.parse = function (expr) { 692 | return new Parser().parse(expr); 693 | }; 694 | 695 | Parser.evaluate = function (expr, variables) { 696 | return Parser.parse(expr).evaluate(variables); 697 | }; 698 | 699 | Parser.Expression = Expression; 700 | 701 | var PRIMARY = 1 << 0; 702 | var OPERATOR = 1 << 1; 703 | var FUNCTION = 1 << 2; 704 | var LPAREN = 1 << 3; 705 | var RPAREN = 1 << 4; 706 | var COMMA = 1 << 5; 707 | //var SIGN = 1 << 6; 708 | var CALL = 1 << 7; 709 | var NULLARY_CALL = 1 << 8; 710 | 711 | Parser.prototype = { 712 | overload: function(key, Class, func){ 713 | if(func.length !== 1 && func.length !== 2){ 714 | throw new Error('Invalid number of arguments, expected 1 or 2.'); 715 | } 716 | if(!this.ops1[key] && !this.ops2[key]){ 717 | throw new Error('Invalid operator, use addOperator to add one.'); 718 | } 719 | var type = func.length; 720 | 721 | this['overload_map' + type] = this['overload_map' + type] || {}; 722 | this['overload_map_funcs' + type] = this['overload_map_funcs' + type] || {}; 723 | 724 | this['overload_map' + type][key] = this['overload_map' + type][key] || []; 725 | this['overload_map_funcs' + type][key] = this['overload_map_funcs' + type][key] || []; 726 | 727 | var overload_map = this['overload_map' + type] [key]; 728 | var overload_map_funcs = this['overload_map_funcs' + type][key]; 729 | var self = this; 730 | 731 | function overload(op){ 732 | op = op || function(){ 733 | throw new Error('function ' + op + ' not defined.'); 734 | }; 735 | return function(){ 736 | var args = [].slice.call(arguments); 737 | var matched = false; 738 | 739 | for(var i = 0; i < args.length; i++){ 740 | var _Class = args[i].constructor; 741 | var idx = overload_map.indexOf(_Class); 742 | 743 | if(idx >= 0){ 744 | matched = true; 745 | break; 746 | } 747 | } 748 | 749 | if(matched){ 750 | args = args.map(function(o){ 751 | if(!(o instanceof _Class)){ 752 | return new _Class(o); 753 | } 754 | return o; 755 | }); 756 | return overload_map_funcs[idx].apply(this, args); 757 | } 758 | 759 | return op.apply(this, args); 760 | } 761 | } 762 | 763 | if(overload_map.length <= 0){ 764 | if(key in this.ops1 && func.length === 1){ 765 | this.overload_ops1[key] = this.ops1[key] = overload(this.ops1[key]); 766 | }else if(key in this.ops2 && func.length === 2){ 767 | this.overload_ops2[key] = this.ops2[key] = overload(this.ops2[key]); 768 | } 769 | } 770 | overload_map.push(Class); 771 | overload_map_funcs.push(func); 772 | }, 773 | 774 | addOperator: function(name, prio, func){ 775 | if(func.length === 1){ 776 | this.overload_ops1[name] = this.ops1[name] = func; 777 | this.tokenprio_map1[name] = prio; 778 | }else if(func.length === 2){ 779 | this.overload_ops2[name] = this.ops2[name] = func; 780 | this.tokenprio_map2[name] = prio; 781 | }else{ 782 | throw new Error('Invalid number of arguments, expected 1 or 2.') 783 | } 784 | }, 785 | 786 | addFunction: function(name, func, can_simplify){ 787 | this.functions[name] = func; 788 | if(can_simplify === false){ 789 | this.simplify_exclude_functions.push(name); 790 | } 791 | }, 792 | 793 | parse: function (expr) { 794 | this.errormsg = ""; 795 | this.success = true; 796 | var operstack = []; 797 | var tokenstack = []; 798 | this.tmpprio = 0; 799 | var expected = (PRIMARY | LPAREN | FUNCTION); 800 | var noperators = 0; 801 | this.expression = expr; 802 | this.pos = 0; 803 | 804 | while (this.pos < this.expression.length) { 805 | if (this.isOp1() && (expected & FUNCTION) !== 0) { 806 | this.tokenprio = this.tokenprio_map1[this.tokenindex]; 807 | noperators++; 808 | this.pos += this.tokenindex.length; 809 | this.addfunc(tokenstack, operstack, TOP1); 810 | expected = (PRIMARY | LPAREN | FUNCTION); 811 | } 812 | else if (this.isSuffixOp()){ 813 | this.tokenprio = this.tokenprio_map1[this.tokenindex]; 814 | this.pos += this.tokenindex.length-1; 815 | noperators++; 816 | this.addfunc(tokenstack, operstack, TOP1); 817 | expected = (OPERATOR | RPAREN | COMMA); 818 | } 819 | else if (this.isOp2()) { 820 | this.tokenprio = this.tokenprio_map2[this.tokenindex]; 821 | this.pos += this.tokenindex.length; 822 | if (this.isComment()) { 823 | 824 | } 825 | else { 826 | if ((expected & OPERATOR) === 0) { 827 | this.error_parsing(this.pos, "unexpected operator"); 828 | } 829 | noperators += 2; 830 | this.addfunc(tokenstack, operstack, TOP2); 831 | expected = (PRIMARY | LPAREN | FUNCTION); 832 | } 833 | } 834 | else if (this.isNumber()) { 835 | if ((expected & PRIMARY) === 0) { 836 | this.error_parsing(this.pos, "unexpected number"); 837 | } 838 | var token = new Token(TNUMBER, 0, 0, this.tokennumber); 839 | tokenstack.push(token); 840 | 841 | expected = (OPERATOR | RPAREN | COMMA); 842 | } 843 | else if (this.isString()) { 844 | if ((expected & PRIMARY) === 0) { 845 | this.error_parsing(this.pos, "unexpected string"); 846 | } 847 | var token = new Token(TNUMBER, 0, 0, this.tokennumber); 848 | tokenstack.push(token); 849 | 850 | expected = (OPERATOR | RPAREN | COMMA); 851 | } 852 | else if (this.isLeftParenth()) { 853 | this.pos++; 854 | this.tmpprio += 10; 855 | 856 | if ((expected & LPAREN) === 0) { 857 | this.error_parsing(this.pos, "unexpected \"(\""); 858 | } 859 | 860 | if (expected & CALL) { 861 | noperators += 2; 862 | this.tokenprio = -2; 863 | this.tokenindex = -1; 864 | this.addfunc(tokenstack, operstack, TFUNCALL); 865 | } 866 | 867 | expected = (PRIMARY | LPAREN | FUNCTION | NULLARY_CALL); 868 | } 869 | else if (this.isRightParenth()) { 870 | this.pos++; 871 | this.tmpprio -= 10; 872 | if (expected & NULLARY_CALL) { 873 | var token = new Token(TNUMBER, 0, 0, []); 874 | tokenstack.push(token); 875 | } 876 | else if ((expected & RPAREN) === 0) { 877 | this.error_parsing(this.pos, "unexpected \")\""); 878 | } 879 | 880 | expected = (OPERATOR | RPAREN | COMMA | LPAREN | CALL); 881 | } 882 | else if (this.isComma()) { 883 | this.pos++; 884 | this.tokenprio = -1; 885 | 886 | if ((expected & COMMA) === 0) { 887 | this.error_parsing(this.pos, "unexpected \",\""); 888 | } 889 | this.addfunc(tokenstack, operstack, TOP2); 890 | noperators += 2; 891 | expected = (PRIMARY | LPAREN | FUNCTION); 892 | } 893 | else if (this.isConst()) { 894 | if ((expected & PRIMARY) === 0) { 895 | this.error_parsing(this.pos, "unexpected constant"); 896 | } 897 | var consttoken = new Token(TNUMBER, 0, 0, this.tokennumber); 898 | tokenstack.push(consttoken); 899 | expected = (OPERATOR | RPAREN | COMMA); 900 | } 901 | else if (this.isVar()) { 902 | if ((expected & PRIMARY) === 0) { 903 | this.error_parsing(this.pos, "unexpected variable"); 904 | } 905 | var vartoken = new Token(TVAR, this.tokenindex, 0, 0); 906 | tokenstack.push(vartoken); 907 | 908 | expected = (OPERATOR | RPAREN | COMMA | LPAREN | CALL); 909 | } 910 | else if (this.isWhite()) { 911 | } 912 | else { 913 | if (this.errormsg === "") { 914 | this.error_parsing(this.pos, "unknown character"); 915 | } 916 | else { 917 | this.error_parsing(this.pos, this.errormsg); 918 | } 919 | } 920 | } 921 | if (this.tmpprio < 0 || this.tmpprio >= 10) { 922 | this.error_parsing(this.pos, "unmatched \"()\""); 923 | } 924 | while (operstack.length > 0) { 925 | var tmp = operstack.pop(); 926 | tokenstack.push(tmp); 927 | } 928 | 929 | if (noperators + 1 !== tokenstack.length) { 930 | //print(noperators + 1); 931 | //print(tokenstack); 932 | this.error_parsing(this.pos, "parity"); 933 | } 934 | 935 | return new Expression( 936 | tokenstack, 937 | this.ops1, 938 | this.ops2, 939 | this.functions, 940 | this.overload_ops1, 941 | this.overload_ops2, 942 | this.simplify_exclude_functions); 943 | }, 944 | 945 | evaluate: function (expr, variables) { 946 | return this.parse(expr).evaluate(variables); 947 | }, 948 | 949 | error_parsing: function (column, msg) { 950 | this.success = false; 951 | this.errormsg = "parse error [column " + (column) + "]: " + msg; 952 | this.column = column; 953 | throw new Error(this.errormsg); 954 | }, 955 | 956 | //\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ 957 | 958 | addfunc: function (tokenstack, operstack, type_) { 959 | var operator = new Token(type_, this.tokenindex, this.tokenprio + this.tmpprio, 0); 960 | while (operstack.length > 0) { 961 | if (operator.prio_ <= operstack[operstack.length - 1].prio_) { 962 | tokenstack.push(operstack.pop()); 963 | } 964 | else { 965 | break; 966 | } 967 | } 968 | operstack.push(operator); 969 | }, 970 | 971 | // Ported from the yajjl JSON parser at http://code.google.com/p/yajjl/ 972 | unescape: function(v, pos) { 973 | var buffer = []; 974 | var escaping = false; 975 | 976 | for (var i = 0; i < v.length; i++) { 977 | var c = v.charAt(i); 978 | 979 | if (escaping) { 980 | switch (c) { 981 | case "'": 982 | buffer.push("'"); 983 | break; 984 | case '\\': 985 | buffer.push('\\'); 986 | break; 987 | case '/': 988 | buffer.push('/'); 989 | break; 990 | case 'b': 991 | buffer.push('\b'); 992 | break; 993 | case 'f': 994 | buffer.push('\f'); 995 | break; 996 | case 'n': 997 | buffer.push('\n'); 998 | break; 999 | case 'r': 1000 | buffer.push('\r'); 1001 | break; 1002 | case 't': 1003 | buffer.push('\t'); 1004 | break; 1005 | case 'u': 1006 | // interpret the following 4 characters as the hex of the unicode code point 1007 | var codePoint = parseInt(v.substring(i + 1, i + 5), 16); 1008 | buffer.push(String.fromCharCode(codePoint)); 1009 | i += 4; 1010 | break; 1011 | default: 1012 | throw this.error_parsing(pos + i, "Illegal escape sequence: '\\" + c + "'"); 1013 | } 1014 | escaping = false; 1015 | } else { 1016 | if (c == '\\') { 1017 | escaping = true; 1018 | } else { 1019 | buffer.push(c); 1020 | } 1021 | } 1022 | } 1023 | 1024 | return buffer.join(''); 1025 | }, 1026 | 1027 | isNumber: function () { 1028 | var r = false; 1029 | var str = ""; 1030 | while (this.pos < this.expression.length) { 1031 | var code = this.expression.charCodeAt(this.pos); 1032 | if ((code >= 48 && code <= 57) || code === 46) { 1033 | str += this.expression.charAt(this.pos); 1034 | this.pos++; 1035 | this.tokennumber = parseFloat(str); 1036 | r = true; 1037 | } 1038 | else { 1039 | break; 1040 | } 1041 | } 1042 | return r; 1043 | }, 1044 | 1045 | isString: function () { 1046 | var r = false; 1047 | var str = ""; 1048 | var startpos = this.pos; 1049 | if (this.pos < this.expression.length && this.expression.charAt(this.pos) == "'") { 1050 | this.pos++; 1051 | while (this.pos < this.expression.length) { 1052 | var code = this.expression.charAt(this.pos); 1053 | if (code != "'" || str.slice(-1) == "\\") { 1054 | str += this.expression.charAt(this.pos); 1055 | this.pos++; 1056 | } 1057 | else { 1058 | this.pos++; 1059 | this.tokennumber = this.unescape(str, startpos); 1060 | r = true; 1061 | break; 1062 | } 1063 | } 1064 | } 1065 | return r; 1066 | }, 1067 | 1068 | isConst: function () { 1069 | var str; 1070 | for (var i in this.consts) { 1071 | if (true) { 1072 | var L = i.length; 1073 | str = this.expression.substr(this.pos, L); 1074 | if (i === str) { 1075 | this.tokennumber = this.consts[i]; 1076 | this.pos += L; 1077 | return true; 1078 | } 1079 | } 1080 | } 1081 | return false; 1082 | }, 1083 | 1084 | isLeftParenth: function () { 1085 | var code = this.expression.charCodeAt(this.pos); 1086 | return code === 40; 1087 | }, 1088 | 1089 | isRightParenth: function () { 1090 | var code = this.expression.charCodeAt(this.pos); 1091 | return code === 41; 1092 | }, 1093 | 1094 | isComma: function () { 1095 | var code = this.expression.charCodeAt(this.pos); 1096 | if (code === 44) { // , 1097 | this.tokenindex = ","; 1098 | return true; 1099 | } 1100 | return false; 1101 | }, 1102 | 1103 | isWhite: function () { 1104 | var code = this.expression.charCodeAt(this.pos); 1105 | if (code === 32 || code === 9 || code === 10 || code === 13) { 1106 | this.pos++; 1107 | return true; 1108 | } 1109 | return false; 1110 | }, 1111 | 1112 | isOp1: function () { 1113 | var rest = this.expression.slice(this.pos); 1114 | var ops = Object.keys(this.ops1).sort(function(a, b){ 1115 | return b.length - a.length; 1116 | }); 1117 | var self = this; 1118 | 1119 | var res = ops.some(function(op){ 1120 | if(rest.indexOf(op) == 0){ 1121 | self.tokenindex = op; 1122 | return true; 1123 | } 1124 | }); 1125 | 1126 | return res; 1127 | }, 1128 | 1129 | isOp2: function () { 1130 | var rest = this.expression.slice(this.pos); 1131 | var ops = Object.keys(this.ops2).sort(function(a, b){ 1132 | return b.length - a.length; 1133 | }); 1134 | 1135 | var self = this; 1136 | 1137 | var res = ops.some(function(op){ 1138 | if(rest.indexOf(op) == 0){ 1139 | self.tokenindex = op; 1140 | return true; 1141 | } 1142 | }); 1143 | 1144 | return res; 1145 | }, 1146 | 1147 | isSuffixOp: function() { 1148 | var _r = this.expression.slice(this.pos); 1149 | var rest = '~' + _r; 1150 | var ops = Object.keys(this.ops1).sort(function(a, b){ 1151 | return b.length - a.length; 1152 | }); 1153 | var self = this; 1154 | 1155 | var res = ops.some(function(op){ 1156 | if(op !== '~' && rest.indexOf(op) == 0){ 1157 | self.tokenindex = op; 1158 | return true; 1159 | } 1160 | }); 1161 | 1162 | if(res){ 1163 | var tokenindex = this.tokenindex; 1164 | var len = this.tokenindex.length - 1; 1165 | len += _r.slice(len).length - _r.slice(len).trimLeft().length; 1166 | 1167 | this.pos += len; 1168 | 1169 | if(this.pos >= this.expression.length 1170 | || this.isRightParenth() || this.isOp2() || this.isComma()){ 1171 | //look up forward to check if this operator is suffix operator or 2-target operator 1172 | this.tokenindex = tokenindex; 1173 | this.pos -= len; 1174 | return true; 1175 | }else{ 1176 | this.pos -= len; 1177 | return false; 1178 | } 1179 | } 1180 | 1181 | return res; 1182 | }, 1183 | 1184 | isVar: function () { 1185 | var str = ""; 1186 | for (var i = this.pos; i < this.expression.length; i++) { 1187 | var c = this.expression.charAt(i); 1188 | if (c.toUpperCase() === c.toLowerCase()) { 1189 | if (i === this.pos || (c != '_' && (c < '0' || c > '9'))) { 1190 | break; 1191 | } 1192 | } 1193 | str += c; 1194 | } 1195 | if (str.length > 0) { 1196 | this.tokenindex = str; 1197 | this.tokenprio = 4; 1198 | this.pos += str.length; 1199 | return true; 1200 | } 1201 | return false; 1202 | }, 1203 | 1204 | isComment: function () { 1205 | var code = this.expression.charCodeAt(this.pos - 1); 1206 | if (code === 47 && this.expression.charCodeAt(this.pos) === 42) { 1207 | this.pos = this.expression.indexOf("*/", this.pos) + 2; 1208 | if (this.pos === 1) { 1209 | this.pos = this.expression.length; 1210 | } 1211 | return true; 1212 | } 1213 | return false; 1214 | } 1215 | }; 1216 | 1217 | scope.Parser = Parser; 1218 | return Parser 1219 | })(typeof exports === 'undefined' ? {} : exports); 1220 | -------------------------------------------------------------------------------- /test/parserSpec.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var Parser = require("../parser.js"); 3 | Parser = Parser.Parser; 4 | 5 | describe("Parser", function() { 6 | describe("#evaluate()", function() { 7 | it("2 ^ x", function() { 8 | expect(Parser.evaluate("2 ^ x", {x: 3})).to.equal(8); 9 | }); 10 | it("2 * x + 1", function() { 11 | expect(Parser.evaluate("2 * x + 1", {x: 3})).to.equal(7); 12 | }); 13 | it("2 + 3 * x", function() { 14 | expect(Parser.evaluate("2 + 3 * x", {x: 4})).to.equal(14); 15 | }); 16 | it("(2 + 3) * x", function() { 17 | expect(Parser.evaluate("(2 + 3) * x", {x: 4})).to.equal(20); 18 | }); 19 | it("2-3^x", function() { 20 | expect(Parser.evaluate("2-3^x", {x: 4})).to.equal(-79); 21 | }); 22 | it("-2-3^x", function() { 23 | expect(Parser.evaluate("-2-3^x", {x: 4})).to.equal(-83); 24 | }); 25 | it("-3^x", function() { 26 | expect(Parser.evaluate("-3^x", {x: 4})).to.equal(-81); 27 | }); 28 | it("(-3)^x", function() { 29 | expect(Parser.evaluate("(-3)^x", {x: 4})).to.equal(81); 30 | }); 31 | }); 32 | describe("#substitute()", function() { 33 | var expr = Parser.parse("2 * x + 1"); 34 | var expr2 = expr.substitute("x", "4 * x"); 35 | it("((2*(4*x))+1)", function() { 36 | expect(expr2.evaluate({ x: 3})).to.equal(25); 37 | }); 38 | }); 39 | describe("#simplify()", function() { 40 | var expr = Parser.parse("x * (y * atan(1))").simplify({ y: 4 }); 41 | it("(x*3.141592653589793)", function() { 42 | expect(expr.toString()).to.equal('(x*3.141592653589793)'); 43 | }); 44 | it("6.283185307179586", function() { 45 | expect(expr.evaluate({ x: 2 })).to.equal(6.283185307179586); 46 | }); 47 | }); 48 | describe("#variables()", function() { 49 | var expr = Parser.parse("x * (y * atan(1))"); 50 | it("['x', 'y']", function() { 51 | expect(expr.variables()).to.have.same.members(['x', 'y']); 52 | }); 53 | it("['x']", function() { 54 | expect(expr.simplify({y: 4}).variables()).to.have.same.members(['x']); 55 | }); 56 | }); 57 | describe("#equal()", function() { 58 | it("2 == 3", function() { 59 | expect(Parser.evaluate("2 == 3")).to.equal(false); 60 | }); 61 | it("3 * 1 == 2", function() { 62 | expect(Parser.evaluate("3 == 2")).to.not.equal(true); 63 | }); 64 | it("3 == 3", function() { 65 | expect(Parser.evaluate("3 == 3")).to.equal(true); 66 | }); 67 | }); 68 | describe("#notEqual()", function() { 69 | it("2 != 3", function() { 70 | expect(Parser.evaluate("2 != 3")).to.equal(true); 71 | }); 72 | it("3 != 2", function() { 73 | expect(Parser.evaluate("3 != 2")).to.not.equal(false); 74 | }); 75 | it("3 != 3", function() { 76 | expect(Parser.evaluate("3 != 3")).to.equal(false); 77 | }); 78 | }); 79 | describe("#greaterThan()", function() { 80 | it("2 > 3", function() { 81 | expect(Parser.evaluate("2 > 3")).to.equal(false); 82 | }); 83 | it("3 > 2", function() { 84 | expect(Parser.evaluate("3 > 2")).to.equal(true); 85 | }); 86 | it("3 > 3", function() { 87 | expect(Parser.evaluate("3 > 3")).to.equal(false); 88 | }); 89 | }); 90 | describe("#greaterThanEqual()", function() { 91 | it("2 >= 3", function() { 92 | expect(Parser.evaluate("2 >= 3")).to.equal(false); 93 | }); 94 | it("3 >= 2", function() { 95 | expect(Parser.evaluate("3 >= 2")).to.equal(true); 96 | }); 97 | it("3 >= 3", function() { 98 | expect(Parser.evaluate("3 >= 3")).to.equal(true); 99 | }); 100 | }); 101 | describe("#lessThan()", function() { 102 | it("2 < 3", function() { 103 | expect(Parser.evaluate("2 < 3")).to.equal(true); 104 | }); 105 | it("3 < 2", function() { 106 | expect(Parser.evaluate("3 < 2")).to.equal(false); 107 | }); 108 | it("3 < 3", function() { 109 | expect(Parser.evaluate("3 < 3")).to.equal(false); 110 | }); 111 | }); 112 | describe("#lessThanEqual()", function() { 113 | it("2 <= 3", function() { 114 | expect(Parser.evaluate("2 <= 3")).to.equal(true); 115 | }); 116 | it("3 <= 2", function() { 117 | expect(Parser.evaluate("3 <= 2")).to.equal(false); 118 | }); 119 | it("3 <= 3", function() { 120 | expect(Parser.evaluate("3 <= 3")).to.equal(true); 121 | }); 122 | }); 123 | describe("#andOperator()", function() { 124 | it("1 and 0", function() { 125 | expect(Parser.evaluate("1 and 0")).to.equal(false); 126 | }); 127 | it("1 and 1", function() { 128 | expect(Parser.evaluate("1 and 1")).to.equal(true); 129 | }); 130 | it("0 and 0", function() { 131 | expect(Parser.evaluate("0 and 0")).to.equal(false); 132 | }); 133 | it("0 and 1", function() { 134 | expect(Parser.evaluate("0 and 1")).to.equal(false); 135 | }); 136 | it("0 and 1 and 0", function() { 137 | expect(Parser.evaluate("0 and 1 and 0")).to.equal(false); 138 | }); 139 | it("1 and 1 and 0", function() { 140 | expect(Parser.evaluate("1 and 1 and 0")).to.equal(false); 141 | }); 142 | }); 143 | describe("#orOperator()", function() { 144 | it("1 or 0", function() { 145 | expect(Parser.evaluate("1 or 0")).to.equal(true); 146 | }); 147 | it("1 or 1", function() { 148 | expect(Parser.evaluate("1 or 1")).to.equal(true); 149 | }); 150 | it("0 or 0", function() { 151 | expect(Parser.evaluate("0 or 0")).to.equal(false); 152 | }); 153 | it("0 or 1", function() { 154 | expect(Parser.evaluate("0 or 1")).to.equal(true); 155 | }); 156 | it("0 or 1 or 0", function() { 157 | expect(Parser.evaluate("0 or 1 or 0")).to.equal(true); 158 | }); 159 | it("1 or 1 or 0", function() { 160 | expect(Parser.evaluate("1 or 1 or 0")).to.equal(true); 161 | }); 162 | }); 163 | describe("#condition()", function() { 164 | it("cond(1, 1, 0)", function() { 165 | expect(Parser.evaluate("cond(1, 1, 0)")).to.equal(1); 166 | }); 167 | it("cond(0, 1, 0)", function() { 168 | expect(Parser.evaluate("cond(0, 1, 0)")).to.equal(0); 169 | }); 170 | it("cond(1==1 or 2==1, 39, 0)", function() { 171 | expect(Parser.evaluate("cond(1==1 or 2==1, 39, 0)")).to.equal(39); 172 | }); 173 | it("cond(1==1 or 1==2, -4 + 8, 0)", function() { 174 | expect(Parser.evaluate("cond(1==1 or 1==2, -4 + 8, 0)")).to.equal(4); 175 | }); 176 | it("cond(3 && 6, cond(45 > 5 * 11, 3 * 3, 2.4), 0)", function() { 177 | expect(Parser.evaluate("cond(3 and 6, cond(45 > 5 * 11, 3 * 3, 2.4), 0)")).to.equal(2.4); 178 | }); 179 | }); 180 | describe("#suffix operator", function(){ 181 | it("5!", function(){ 182 | expect(Parser.evaluate("5!")).to.equal(120); 183 | }); 184 | it("(3!)!", function(){ 185 | expect(Parser.evaluate("(3!)!")).to.equal(720); 186 | }); 187 | it("percentage", function(){ 188 | var parser = new Parser(); 189 | parser.addOperator("~%", 4, function(a){ 190 | return a / 100; 191 | }); 192 | expect(parser.evaluate("5%")).to.equal(0.05); 193 | expect(parser.evaluate("2^5%")).to.equal(0.32); 194 | expect(parser.evaluate("x+y%", {x:1, y:2})).to.equal(1.02); 195 | }); 196 | }); 197 | describe("commas", function(){ 198 | it("(1,2)+(3,4)", function(){ 199 | expect(Parser.evaluate("(1,2)+(3,4)")).to.equal(6); 200 | }); 201 | it("max(1,(2,4),3)", function(){ 202 | expect(Parser.evaluate("max(1,(2,4),3)")).to.equal(4); 203 | }); 204 | it("max((1,(2,4),3))", function(){ 205 | expect(Parser.evaluate("max((1,(2,4),3))")).to.equal(3); 206 | }); 207 | }); 208 | describe("operator overloading", function(){ 209 | function Complex(r, i){ 210 | this.r = r; 211 | this.i = i || 0; 212 | } 213 | function Vector(x, y){ 214 | this.x = x; 215 | this.y = y; 216 | } 217 | var parser = new Parser(); 218 | parser.overload("+", Complex, function(a, b){ 219 | return new Complex(a.r + b.r, a.i + b.i); 220 | }); 221 | parser.overload("-", Complex, function(a,b){ 222 | return new Complex(a.r-b.r, a.i-b.i); 223 | }); 224 | 225 | parser.overload("-", Complex, function(a){ 226 | return new Complex(-a.r, -a.i); 227 | }); 228 | 229 | parser.addOperator("**", 4, function(a, b){ //vector cross 230 | return new Vector(a.x * b.y, - a.y * b.x); 231 | }); 232 | 233 | it("Complex + Complex", function(){ 234 | var a = new Complex(1, 2); 235 | var b = new Complex(3, 4); 236 | expect(parser.evaluate("a + b", {a:a, b:b})).to.deep.equal({ 237 | r: 4, 238 | i: 6 239 | }); 240 | }); 241 | it("Complex + Real + Complex", function(){ 242 | var a = new Complex(1, 2); 243 | var b = new Complex(3, 4); 244 | expect(parser.evaluate("a + 1 + b", {a:a, b:b})).to.deep.equal({ 245 | r: 5, 246 | i: 6 247 | }); 248 | }); 249 | it("Complex + (Real + Complex)", function(){ 250 | var a = new Complex(1, 2); 251 | var b = new Complex(3, 4); 252 | expect(parser.evaluate("a + (1 + b)", {a:a, b:b})).to.deep.equal({ 253 | r: 5, 254 | i: 6 255 | }); 256 | }); 257 | it("-Complex + Complex - Real", function(){ 258 | var expr = parser.parse("-a + b - 1"); 259 | var a = new Complex(1, 2); 260 | var b = new Complex(3, 4); 261 | expect(expr.evaluate({a:a, b:b})).to.deep.equal({ 262 | r: 1, 263 | i: 2 264 | }); 265 | }); 266 | it("Vector cross", function(){ 267 | var a = new Vector(1, 2); 268 | var b = new Vector(3, 4); 269 | expect(parser.evaluate("a ** b", {a:a, b:b})).to.deep.equal({ 270 | x: 4, 271 | y: -6 272 | }); 273 | }); 274 | }); 275 | describe("toJSFunction", function(){ 276 | it("toJSFunction", function(){ 277 | var expr = Parser.parse("x ^ 2 + y ^ 2 + 1"); 278 | var func1 = expr.toJSFunction(['x', 'y']); 279 | var func2 = expr.toJSFunction(['x'], {y: 2}); 280 | 281 | expect(func1(1, 1)).to.equal(3); 282 | expect(func2(2)).to.equal(9); 283 | }); 284 | }); 285 | describe("addFunction", function(){ 286 | var parser = new Parser(); 287 | parser.addFunction('time', function(){ 288 | return Date.now(); 289 | },false); 290 | 291 | it("time function", function(){ 292 | var expr = parser.parse("'abc?t='+time()"); 293 | expect(expr.evaluate().length).to.equal(19); 294 | var expr2 = expr.simplify(); 295 | expect(expr2.toString()).to.equal("('abc?t='+time())"); 296 | }); 297 | 298 | parser.addFunction('xor', function(a, b){ 299 | return a ^ b; 300 | }); 301 | 302 | it("xor(5, 7) + x + 1", function(){ 303 | var expr = parser.parse("xor(5, 7) + x + 1"); 304 | expect(expr.simplify().toString()).to.equal("((2+x)+1)"); 305 | expect(expr.toJSFunction(['x'])(2)).to.equal(5); 306 | }); 307 | }); 308 | }); 309 | --------------------------------------------------------------------------------