├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── echo.js ├── func.js ├── gulpfile.js ├── lex.js ├── license ├── package.json ├── parse.js ├── readme.md ├── test.js └── values.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowSpacesInNamedFunctionExpression": { 3 | "beforeOpeningRoundBrace": true 4 | }, 5 | "disallowSpacesInFunctionExpression": { 6 | "beforeOpeningRoundBrace": true 7 | }, 8 | "disallowSpacesInAnonymousFunctionExpression": { 9 | "beforeOpeningRoundBrace": true 10 | }, 11 | "disallowSpacesInFunctionDeclaration": { 12 | "beforeOpeningRoundBrace": true 13 | }, 14 | "disallowEmptyBlocks": true, 15 | "disallowKeywordsOnNewLine": [ "else" ], 16 | "disallowQuotedKeysInObjects": true, 17 | "disallowSpacesInsideArrayBrackets": true, 18 | "disallowSpacesInsideParentheses": true, 19 | "disallowSpaceAfterObjectKeys": true, 20 | "disallowSpaceAfterPrefixUnaryOperators": true, 21 | "disallowSpaceBeforePostfixUnaryOperators": true, 22 | "disallowSpaceBeforeBinaryOperators": [ 23 | "," 24 | ], 25 | "disallowMixedSpacesAndTabs": true, 26 | "disallowTrailingWhitespace": true, 27 | "disallowYodaConditions": true, 28 | "disallowKeywords": [ "with" ], 29 | "disallowMultipleLineStrings": true, 30 | "requireSpaceBeforeBlockStatements": true, 31 | "requireSpacesInConditionalExpression": true, 32 | "requireBlocksOnNewline": 1, 33 | "requireCommaBeforeLineBreak": true, 34 | "requireSpaceBeforeBinaryOperators": true, 35 | "requireSpaceAfterBinaryOperators": true, 36 | "requireCamelCaseOrUpperCaseIdentifiers": true, 37 | "requireLineFeedAtFileEnd": true, 38 | "requireOperatorBeforeLineBreak": true, 39 | "requireCapitalizedConstructors": true, 40 | "requireCurlyBraces": [ 41 | "if", 42 | "else", 43 | "for", 44 | "while", 45 | "do", 46 | "try", 47 | "catch" 48 | ], 49 | "requireSpaceAfterKeywords": [ 50 | "if", 51 | "else", 52 | "for", 53 | "while", 54 | "do", 55 | "switch", 56 | "case", 57 | "return", 58 | "try", 59 | "catch", 60 | "typeof" 61 | ], 62 | "maximumLineLength": { 63 | "value": 115, 64 | "allowComments": true, 65 | "allowRegex": true 66 | }, 67 | "safeContextKeyword": "self", 68 | "validateIndentation": 2 69 | } 70 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | /* see www.jshint.com/docs/options/ */ 3 | "bitwise": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "es3": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "quotmark": "single", 14 | "undef": true, 15 | "maxlen": 124, 16 | "unused": true, 17 | "globals" : { 18 | /* suppress warnings for npm/browserify stuff */ 19 | "__dirname": false, 20 | "exports": false, 21 | "module": false, 22 | "require": false, 23 | "describe": false, 24 | "it": false 25 | }, 26 | "devel": true, 27 | "browser": true 28 | } 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.11 4 | - 0.10 5 | -------------------------------------------------------------------------------- /echo.js: -------------------------------------------------------------------------------- 1 | /* get back the (hopefully) exact input expression from the AST */ 2 | 3 | var R = require('ramda'); 4 | var parse = require('./parse'); 5 | 6 | 7 | var template = function(str, vals) { 8 | var valIdx = 0; 9 | return str.replace(/#/g, function() { return vals[valIdx++]; }); 10 | }; 11 | 12 | 13 | var _echo = function _echo(node) { 14 | return template(node.template, R.map(_echo, node.children)); 15 | }; 16 | 17 | 18 | var echo = R.pipe(parse, _echo); 19 | echo.fromAST = _echo; 20 | module.exports = echo; 21 | -------------------------------------------------------------------------------- /func.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'); 2 | var parse = require('./parse'); 3 | 4 | 5 | var transformers = { 6 | expr: function(node) { 7 | return { c: function(vals) { return '(' + vals[0] + ')'; }, 8 | subs: node.children }; 9 | }, 10 | literal: function(node) { 11 | var str = '' + node.options.value; 12 | if (!R.contains('.', str)) { 13 | str += '.0'; 14 | } 15 | return { c: function() { return str; }, 16 | subs: [] }; 17 | }, 18 | name: function(node) { 19 | var key = node.options.key, 20 | upperKey = key.toUpperCase(); 21 | if (R.prop(upperKey, Math)) { 22 | return { 23 | c: function() { 24 | return 'Math.' + upperKey; 25 | }, 26 | subs: [] 27 | }; 28 | } 29 | return { 30 | c: function() { 31 | return 'symbols["' + key + '"]'; 32 | }, 33 | subs: [] 34 | }; 35 | }, 36 | func: function(node) { 37 | var key = node.options.key; 38 | var narys = { 39 | product: '*', 40 | div: '/', 41 | mod: '%', 42 | sum: '+', 43 | minus: '-', 44 | lessThan: '<', 45 | greaterThan: '>' 46 | }; 47 | if (narys[key]) { 48 | return { 49 | c: function(args) { return '(' + args.join(narys[key]) + ')'; }, 50 | subs: node.children 51 | }; 52 | } 53 | if (key === 'neg') { 54 | return { 55 | c: function(args) { return '(-' + args[0] + ')'; }, 56 | subs: node.children 57 | }; 58 | } 59 | if (R.prop(key, Math)) { 60 | return { 61 | c: function(args) { return 'Math.' + key + '(' + args.join(',') + ')'; }, 62 | subs: node.children 63 | }; 64 | } 65 | return { c: function(args) { return 'symbols["' + node.options.key + '"]' + 66 | '(' + args.join(',') + ')'; }, subs: node.children }; 67 | } 68 | }; 69 | 70 | 71 | var compileAST = function comp(ASTNode) { 72 | var transformer = transformers[ASTNode.node](ASTNode); 73 | return transformer.c(R.map(comp, transformer.subs)); 74 | }; 75 | 76 | 77 | var functionify = function(expr) { 78 | /* istanbul ignore next */ 79 | var templateFn = function(symbols, expr) { 80 | symbols = symbols || {}; 81 | return expr; 82 | }; 83 | var body = templateFn 84 | .toString() 85 | .split('\n') 86 | .slice(1, -1) // drop function header and closing } 87 | .join('\n') 88 | .replace('expr', expr); 89 | /* jslint evil:true */ 90 | return new Function('symbols', body); 91 | /* jslint evil:false */ 92 | }; 93 | 94 | 95 | var compile = R.pipe(parse, compileAST, functionify); 96 | compile.fromAST = R.pipe(compileAST, functionify); 97 | compile.express = R.pipe(parse, compileAST); 98 | compile.express.fromAST = compileAST; 99 | 100 | module.exports = compile; 101 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var $ = require('gulp-load-plugins')(); 3 | var mocha = require('gulp-mocha'); 4 | 5 | 6 | var fail = function(err) { throw err; }; 7 | 8 | 9 | gulp.task('mocha', function() { 10 | return gulp.src('test.js', {read: false}) 11 | .pipe(mocha({reporter: 'dot'})) 12 | .on('error', fail); 13 | }); 14 | 15 | gulp.task('jshint', function() { 16 | return gulp.src('*.js') 17 | .pipe($.jshint()) 18 | .pipe($.jshint.reporter('jshint-stylish')) 19 | .pipe($.jshint.reporter('fail')) 20 | .on('error', fail); 21 | }); 22 | 23 | gulp.task('jscs', function() { 24 | return gulp.src('*.js') 25 | .pipe($.jscs()) 26 | .on('error', fail); 27 | }); 28 | 29 | 30 | gulp.task('test', ['mocha', 'jshint', 'jscs']); 31 | -------------------------------------------------------------------------------- /lex.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'); 2 | 3 | var tokenMatches = [ 4 | { type: 'space', match: /^\s+/ }, 5 | { type: 'literal', match: /^(\d*\.\d+|\d+)/ }, 6 | { type: 'name', match: /^[a-zA-Z_]\w*/ }, 7 | { type: 'paren', match: /^[\(\)]/ }, 8 | { type: 'operator', match: /^[\+\-%\*\/\^<>]/ } 9 | ]; 10 | 11 | 12 | var mkToken = function(token, value, repr) { 13 | return { 14 | type: 'token', 15 | token: token, 16 | value: value, 17 | repr: repr || value 18 | }; 19 | }; 20 | 21 | 22 | var checkToken = function(token, checks) { 23 | checks = checks || {}; 24 | return !!token && token.type === 'token' && 25 | (token.token === checks.token || !checks.token) && 26 | (token.value === checks.value || !checks.value); 27 | }; 28 | 29 | 30 | var chomp = function(str) { 31 | var matcher = R.find(R.where({ match: function(re) { return re.test(str); } }), tokenMatches); 32 | var token = matcher ? 33 | mkToken(matcher.type, str.match(matcher.match)[0]) : 34 | mkToken(null, str[0]); 35 | return { token: token, leftover: str.slice(token.value.length) }; 36 | }; 37 | 38 | 39 | var chompReduce = function(str) { 40 | function reducer(initial, leftover) { 41 | if (!leftover) { return initial; } // nothing more to eat! 42 | var result = chomp(leftover); 43 | return reducer(R.append(result.token, initial), result.leftover); 44 | } 45 | return reducer([], str); 46 | }; 47 | 48 | 49 | chompReduce.token = mkToken; 50 | chompReduce.check = checkToken; 51 | 52 | module.exports = chompReduce; 53 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Phil Schleihauf 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expression-parser", 3 | "repository": "git@github.com:uniphil/expression-parser.git", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "func.js", 7 | "scripts": { 8 | "test": "gulp test && npm run coverage", 9 | "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly; cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" 10 | }, 11 | "author": "uniphil", 12 | "license": "BSD", 13 | "devDependencies": { 14 | "chai": "^1.9.2", 15 | "coveralls": "^2.11.2", 16 | "gulp": "^3.8.8", 17 | "gulp-jscs": "^1.2.0", 18 | "gulp-jshint": "^1.8.5", 19 | "gulp-load-plugins": "^0.7.0", 20 | "gulp-mocha": "^1.1.1", 21 | "istanbul": "^0.3.4", 22 | "jshint-stylish": "^1.0.0", 23 | "mocha": "^1.21.4" 24 | }, 25 | "dependencies": { 26 | "ramda": "^0.8.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | var R = require('ramda'); 2 | var lex = require('./lex'); 3 | 4 | 5 | function ParseError(message) { 6 | if (!(this instanceof ParseError)) { 7 | throw new Error('Error throwing parse error -- pleas use the "new" ' + 8 | ' keyword. The parse error was:' + message); 9 | } 10 | this.toString = function() { 11 | return message; 12 | }; 13 | } 14 | 15 | 16 | var parseTokens; // implemented later; declared here because we recurse to it 17 | 18 | 19 | var astNode = function(nodeType, children, options) { 20 | children = children || []; 21 | options = options || {}; 22 | var template = options.template || R.repeatN('#', children.length).join(''); 23 | delete options.template; 24 | return { 25 | id: options.id || undefined, 26 | type: 'ASTNode', 27 | node: nodeType, 28 | template: template, 29 | children: children, 30 | options: options 31 | }; 32 | }; 33 | 34 | 35 | var parenDepthMod = function(token) { 36 | return (token.value === '(') ? +1 : -1; 37 | }; 38 | 39 | 40 | var pullSubExpressions = function(tokens) { 41 | var token, 42 | parenDepth = 0, 43 | subExprTokens, 44 | subAST, 45 | outputTokens = [], 46 | openTempl; 47 | 48 | for (var i = 0; i < tokens.length; i++) { 49 | token = tokens[i]; 50 | if (parenDepth === 0) { 51 | if (lex.check(token, {token: 'paren'})) { 52 | if (lex.check(token, {value: ')'})) { throw new ParseError('Unexpected close paren ")"'); } 53 | parenDepth += 1; 54 | subExprTokens = []; 55 | openTempl = token.repr; 56 | } else { 57 | outputTokens.push(token); 58 | } 59 | } else { 60 | if (lex.check(token, {token: 'paren'})) { 61 | parenDepth += parenDepthMod(token); 62 | if (parenDepth === 0) { 63 | subAST = parseTokens(subExprTokens); 64 | subAST.template = openTempl + subAST.template + token.repr; 65 | outputTokens.push(subAST); 66 | } else { 67 | subExprTokens.push(token); 68 | } 69 | } else { 70 | subExprTokens.push(token); 71 | } 72 | } 73 | } 74 | if (parenDepth !== 0) { throw new ParseError('Unclosed paren'); } 75 | return outputTokens; 76 | }; 77 | 78 | 79 | var stepTrios = function(puller) { 80 | // steps right to left 81 | 82 | var getTrio = function(leftovers, tokens) { 83 | return [ 84 | leftovers.pop() || tokens.pop(), 85 | leftovers.pop() || tokens.pop(), 86 | leftovers.pop() || tokens.pop() 87 | ].reverse(); 88 | }; 89 | 90 | return function(tokens) { 91 | var pulled = [[], null], 92 | output = [], 93 | trio; 94 | while (true) { 95 | trio = getTrio(pulled[0], tokens); 96 | if (!trio[1]) { 97 | if (trio[2]) { output.unshift(trio[2]); } 98 | break; 99 | } 100 | pulled = puller.apply(null, trio); 101 | if (pulled[1] !== null) { 102 | output.unshift(pulled[1]); 103 | } 104 | } 105 | return output; 106 | }; 107 | }; 108 | 109 | 110 | var pullSpaces = stepTrios(function(tL, t, tR) { 111 | tR = R.cloneObj(tR); 112 | var templProp; 113 | 114 | if (lex.check(tR, {token: 'space'})) { 115 | // the very first (right-most) token could be whitespace 116 | t = R.cloneObj(t); 117 | templProp = lex.check(t) ? 'repr' : 'template'; 118 | t[templProp] = t[templProp] + tR.repr; 119 | return [[tL, t], null]; 120 | } 121 | if (lex.check(t, {token: 'space'})) { 122 | if (tL && (lex.check(tR, {token: 'literal'}) || lex.check(tR, {token: 'name'}))) { 123 | templProp = lex.check(tL) ? 'repr' : 'template'; 124 | tL[templProp] += t.repr; 125 | return [[tL], tR]; 126 | } 127 | templProp = lex.check(tR) ? 'repr' : 'template'; 128 | tR[templProp] = t.repr + tR[templProp]; 129 | return [[tL], tR]; 130 | } else { 131 | return [[tL, t], tR]; 132 | } 133 | }); 134 | 135 | 136 | var assertNotOpToken = R.forEach(function(token) { 137 | if (lex.check(token, {token: 'operator'})) { 138 | throw new ParseError('sequential operator: ' + token.repr); 139 | } 140 | }); 141 | 142 | 143 | var pullOps = function(symbol, ary, funcName, options) { 144 | var arys = { 145 | unary: function(tL, t, tR) { 146 | assertNotOpToken([tR]); 147 | var node = astNode('func', [tR], 148 | { key: funcName, template: t.repr + '#' }); 149 | if (options.binarify && 150 | tL && // it's not the first token 151 | !lex.check(tL, {token: 'operator'}) && // thing before isn't an op 152 | !(tL.type === 'astNode' && tL.node === 'func')) { // thing before isn't an op 153 | var injectedToken = lex.token('operator', options.binarify); 154 | injectedToken.repr = ''; 155 | return [[tL, injectedToken, node], null]; 156 | } 157 | return [[tL, node], null]; 158 | }, 159 | binary: function(tL, t, tR) { 160 | assertNotOpToken([tL, tR]); 161 | var node = astNode('func', [tL, tR], 162 | { key: funcName, template: '#' + t.repr + '#' }); 163 | return [[node], null]; 164 | }, 165 | nary: function(tL, t, tR) { 166 | if (tR.type === 'ASTNode' && tR.node === 'func' && tR.options.key === funcName) { 167 | assertNotOpToken([tL, tR]); 168 | tR.children.unshift(tL); 169 | tR.template = '#' + t.repr + tR.template; 170 | return [[tR], null]; 171 | } 172 | return arys.binary(tL, t, tR); 173 | } 174 | }; 175 | 176 | return stepTrios(function(tL, t, tR) { 177 | if (lex.check(t, {token: 'operator', value: symbol})) { 178 | return arys[ary](tL, t, tR); 179 | } 180 | return [[tL, t], tR]; 181 | }); 182 | }; 183 | 184 | 185 | var pullFunctions = stepTrios(function(tL, t, tR) { 186 | // find [name, expr]s, and swap as fn(key=name, children=expr.children) 187 | if (lex.check(t, {token: 'name'}) && tR.node === 'expr') { 188 | return [[tL], astNode('func', tR.children, { 189 | key: t.value, 190 | template: t.repr + tR.template })]; 191 | } 192 | return [[tL, t], tR]; 193 | }); 194 | 195 | 196 | var pullValues = R.map(function(token) { 197 | if (lex.check(token, {token: 'name'})) { 198 | return astNode('name', [], 199 | { key: token.value, template: token.repr }); 200 | } else if (lex.check(token, {token: 'literal'})) { 201 | return astNode('literal', [], 202 | { value: parseFloat(token.value), template: token.repr }); 203 | } 204 | return token; 205 | }); 206 | 207 | 208 | var validateOperators = function(tokens) { 209 | var first = tokens[0], 210 | last = tokens[tokens.length - 1]; 211 | if (lex.check(first, {token: 'operator'}) && !lex.check(first, {value: '-'})) { 212 | throw new ParseError('non-unary leading operator: ' + first.value); 213 | } 214 | if (lex.check(last, {token: 'operator'})) { 215 | throw new ParseError('trailing operator: ' + last.value); 216 | } 217 | return tokens; 218 | }; 219 | 220 | 221 | var failOnBadTokens = function(tokens) { 222 | R.forEach(function(token) { 223 | throw new ParseError('could not parse token: ' + token.value); 224 | }, R.filter(R.where({token: null}), tokens)); 225 | return tokens; 226 | }; 227 | 228 | 229 | var actuallyEqualOps = function(wasLower, wasHigher) { 230 | function flipWithLastChild(node) { 231 | var newHeir = node.children.pop(), // should be the last child! does not handle nAry 232 | switchedAtBirth = newHeir.children.shift(); 233 | node.children.push(switchedAtBirth); 234 | newHeir.children.unshift(node); 235 | return newHeir; 236 | } 237 | 238 | function lookForFlips(node) { 239 | if (lex.check(node)) { return node; } 240 | if (node.node === 'func' && node.options.key === wasLower && 241 | node.children[1] && node.children[1].node === 'func' && node.children[1].options.key === wasHigher) { 242 | node = flipWithLastChild(node); 243 | } 244 | R.forEach(lookForFlips, node.children); 245 | return node; 246 | } 247 | 248 | return R.map(lookForFlips); 249 | }; 250 | 251 | 252 | var pullRoot = function(tokens) { 253 | var template = ''; 254 | if (lex.check(tokens[0], {token: 'space'})) { 255 | template = tokens.shift().repr + template; 256 | } 257 | template += R.repeatN('#', tokens.length || 0).join(''); 258 | return astNode('expr', tokens, {template: template}); 259 | }; 260 | 261 | 262 | var stampIds = function(rootNode) { 263 | var i = 0; 264 | (function stamper(node) { 265 | node.id = i++; 266 | R.forEach(stamper, node.children); 267 | })(rootNode); 268 | return rootNode; 269 | }; 270 | 271 | 272 | parseTokens = R.pipe( 273 | pullSubExpressions, 274 | pullSpaces, 275 | pullFunctions, 276 | pullValues, 277 | validateOperators, 278 | pullOps('^', 'binary', 'pow'), 279 | pullOps('-', 'unary', 'neg', {binarify: '+'}), 280 | pullOps('*', 'nary', 'product'), 281 | pullOps('/', 'binary', 'div'), 282 | pullOps('%', 'binary', 'mod'), 283 | pullOps('+', 'nary', 'sum'), 284 | pullOps('<', 'binary', 'lessThan'), 285 | pullOps('>', 'binary', 'greaterThan'), 286 | actuallyEqualOps('div', 'product'), // makes * and / have equal precedence 287 | pullRoot 288 | ); 289 | 290 | var parse = R.pipe( 291 | lex, 292 | failOnBadTokens, 293 | parseTokens, 294 | stampIds 295 | ); 296 | 297 | 298 | parse.ParseError = ParseError; 299 | parse.astNode = astNode; 300 | parse.lex = lex; 301 | parse.fromTokens = parseTokens; 302 | 303 | 304 | module.exports = parse; 305 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Expression Parser 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.org/uniphil/expression-parser.svg?branch=master)](https://travis-ci.org/uniphil/expression-parser) 5 | [![Coverage Status](https://img.shields.io/coveralls/uniphil/expression-parser.svg)](https://coveralls.io/r/uniphil/expression-parser) 6 | 7 | Parse math expressions to a useful AST, with built-in compilers for: 8 | * creating a sanitized executable javascript function 9 | * creating a function that returns a value for every node of the AST when executed 10 | * echoes back the original expression if parsing succeeds 11 | 12 | The compilers are provided for convenience, an will not be pulled into a build unless you specifically `require` them. The AST is pretty easy to walk if you build your own compiler -- the echo compiler only requires [seven lines of code](https://github.com/uniphil/expression-parser/blob/fa7a0e2a9207fd48752d3376dc624f2b9b58d31a/echo.js#L7-L15) to implement 13 | 14 | 15 | Install 16 | ------- 17 | 18 | ```bash 19 | $ npm install expression-parser 20 | ``` 21 | 22 | 23 | Usage 24 | ----- 25 | 26 | ### Safely execute arbitrary math expression 27 | 28 | and also get the raw js generated for the function 29 | 30 | ```node 31 | > var mkFunc = require('expression-parser/func'); 32 | > var expressionFunc = compile('c*sin(2*t)+1'); 33 | > expressionFunc({c: 0.5}); 34 | 0.9999999999999999 35 | > mkFunc.express('sqrt(x^2 + y^2)') 36 | '(Math.sqrt((Math.pow(symbols["x"],2.0)+Math.pow(symbols["y"],2.0))))' 37 | ``` 38 | 39 | Note that everything in the global `Math` is made available by the built-in function compiler, and it is assumed that the global `Math` assumed available by its `express` function. 40 | 41 | 42 | ### Get an Abstract Syntax Tree from a math expression 43 | 44 | and then echo back the original expression with just the AST 45 | 46 | ```node 47 | > var parse = require('expression-parser/parse'); 48 | > var ast = parse('sin(t)^2 + cos(t)^2'); 49 | > ast 50 | { id: 0, 51 | type: 'ASTNode', 52 | node: 'expr', 53 | template: '#', 54 | children: 55 | [ { id: 1, 56 | type: 'ASTNode', 57 | node: 'func', 58 | template: '# +#', 59 | children: [Object], 60 | options: [Object] } ], 61 | options: {} } 62 | > console.log(JSON.stringify(ast, null, 2)); // print out the whole thing 63 | { 64 | "id": 0, 65 | "type": "ASTNode", 66 | "node": "expr", 67 | "template": "#", 68 | "children": [ 69 | { 70 | "id": 1, 71 | "type": "ASTNode", 72 | "node": "func", 73 | "template": "# +#", 74 | "children": [ 75 | { 76 | "id": 2, 77 | "type": "ASTNode", 78 | "node": "func", 79 | "template": "#^#", 80 | "children": [ 81 | { 82 | "id": 3, 83 | "type": "ASTNode", 84 | "node": "func", 85 | "template": "sin(#)", 86 | "children": [ 87 | { 88 | "id": 4, 89 | "type": "ASTNode", 90 | "node": "name", 91 | "template": "t", 92 | "children": [], 93 | "options": { 94 | "key": "t" 95 | } 96 | } 97 | ], 98 | "options": { 99 | "key": "sin" 100 | } 101 | }, 102 | { 103 | "id": 5, 104 | "type": "ASTNode", 105 | "node": "literal", 106 | "template": "2", 107 | "children": [], 108 | "options": { 109 | "value": 2 110 | } 111 | } 112 | ], 113 | "options": { 114 | "key": "pow" 115 | } 116 | }, 117 | { 118 | "id": 6, 119 | "type": "ASTNode", 120 | "node": "func", 121 | "template": "#^#", 122 | "children": [ 123 | { 124 | "id": 7, 125 | "type": "ASTNode", 126 | "node": "func", 127 | "template": " cos(#)", 128 | "children": [ 129 | { 130 | "id": 8, 131 | "type": "ASTNode", 132 | "node": "name", 133 | "template": "t", 134 | "children": [], 135 | "options": { 136 | "key": "t" 137 | } 138 | } 139 | ], 140 | "options": { 141 | "key": "cos" 142 | } 143 | }, 144 | { 145 | "id": 9, 146 | "type": "ASTNode", 147 | "node": "literal", 148 | "template": "2", 149 | "children": [], 150 | "options": { 151 | "value": 2 152 | } 153 | } 154 | ], 155 | "options": { 156 | "key": "pow" 157 | } 158 | } 159 | ], 160 | "options": { 161 | "key": "sum" 162 | } 163 | } 164 | ], 165 | "options": {} 166 | } 167 | > var valuer = require('expression-parser/values'); 168 | > values.fromAST(ast)({t: 0}) 169 | [ 1, 170 | 1, 171 | 0, 172 | 0, 173 | 0, 174 | 2, 175 | 1, 176 | 1, 177 | 0, 178 | 2 ] 179 | > var echoer = require('expression-parser/echo'); 180 | > echoer.fromAST(ast); 181 | 'sin(t)^2 + cos(t)^2' 182 | ``` 183 | 184 | 185 | Parsing 186 | ------- 187 | 188 | New AST node design: 189 | 190 | ```javascript 191 | { 192 | id: 0, 193 | type: 'ASTNode', 194 | node: 'expr', 195 | template: '#', 196 | children: [], 197 | options: {} 198 | } 199 | ``` 200 | 201 | * `id` must be unique 202 | * `type` must be a valid node type 203 | * `options` contains different properties depending on the node type 204 | 205 | ### AST Node Types 206 | 207 | #### `expr` 208 | 209 | options: 210 | ```javascript 211 | {} 212 | ``` 213 | 214 | #### `func` 215 | 216 | options: 217 | ```javascript 218 | { 219 | key: 'funcName' 220 | } 221 | ``` 222 | 223 | Infix functions should be normal function nodes. Here are some examples for `+` with 2 or 3 operands: 224 | 225 | ```javascript 226 | { 227 | id: 0, 228 | type: 'ASTNode', 229 | node: 'func',u 230 | template: '# + # + #', 231 | children: [someNode, anotherNode, andAnother], 232 | options: { 233 | key: 'sum' 234 | } 235 | } 236 | ``` 237 | 238 | Normal functions can take multiple arguments 239 | ```javascript 240 | { 241 | id: 0, 242 | type: 'ASTNode', 243 | node: 'func', 244 | template: 'min(#, #)', 245 | children: [someNode, anotherNode], 246 | options: { 247 | key: 'min' 248 | } 249 | } 250 | ``` 251 | 252 | 253 | #### `name` 254 | 255 | options: 256 | ```javascript 257 | { 258 | key: 'varName' 259 | } 260 | ``` 261 | 262 | ### `literal` 263 | 264 | options: 265 | ```javascript 266 | { 267 | value: 1 268 | } 269 | ``` 270 | 271 | 272 | License 273 | ------- 274 | [MIT](license) 275 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | require('mocha'); 2 | var assert = require('chai').assert; 3 | 4 | var lex = require('./lex'); 5 | var parse = require('./parse'); 6 | var compileE = require('./echo'); 7 | var compileF = require('./func'); 8 | var compileV = require('./values'); 9 | 10 | 11 | describe('API', function() { 12 | describe('parser', function() { 13 | it('should export a function', function() { 14 | assert.typeOf(parse, 'function'); 15 | }); 16 | it('should expose the lexer', function() { 17 | assert.equal(parse.lex, lex); 18 | }); 19 | it('should accept tokens via fromToken', function() { 20 | assert.typeOf(parse.fromTokens, 'function'); 21 | }); 22 | }); 23 | describe('compilers', function() { 24 | it('should export a function', function() { 25 | assert.typeOf(compileE, 'function'); 26 | assert.typeOf(compileF, 'function'); 27 | assert.typeOf(compileV, 'function'); 28 | }); 29 | it('should accept an AST via fromAST', function() { 30 | assert.typeOf(compileE.fromAST, 'function'); 31 | assert.typeOf(compileF.fromAST, 'function'); 32 | assert.typeOf(compileV.fromAST, 'function'); 33 | }); 34 | }); 35 | }); 36 | 37 | 38 | describe('Lexer', function() { 39 | describe('individual symbols', function() { 40 | it('should detect the right type of token', function() { 41 | assert.propertyVal(lex(' ')[0], 'token', 'space', 'spaces are spaces'); 42 | assert.propertyVal(lex('0')[0], 'token', 'literal', 'zero is a literal'); 43 | assert.propertyVal(lex('1.5')[0], 'token', 'literal', '1.5 is a literal'); 44 | assert.propertyVal(lex('a')[0], 'token', 'name', '"a" is a name'); 45 | assert.propertyVal(lex('+')[0], 'token', 'operator', 'plus is an operator'); 46 | assert.propertyVal(lex('-')[0], 'token', 'operator', 'minus is an operator'); 47 | assert.propertyVal(lex('%')[0], 'token', 'operator', 'mod is an operator'); 48 | assert.propertyVal(lex('*')[0], 'token', 'operator', 'multiply is an operator'); 49 | assert.propertyVal(lex('/')[0], 'token', 'operator', 'divide is an operator'); 50 | assert.propertyVal(lex('^')[0], 'token', 'operator', 'caret is an operator'); 51 | assert.propertyVal(lex('<')[0], 'token', 'operator', 'less than is an operator'); 52 | assert.propertyVal(lex('>')[0], 'token', 'operator', 'greater than is an operator'); 53 | assert.propertyVal(lex('(')[0], 'token', 'paren', 'open parenthesis is a paren'); 54 | assert.propertyVal(lex(')')[0], 'token', 'paren', 'close parenthesis is a paren'); 55 | }); 56 | it('should keep the right value', function() { 57 | assert.propertyVal(lex(' ')[0], 'value', ' '); 58 | assert.propertyVal(lex('a')[0], 'value', 'a'); 59 | assert.propertyVal(lex('foo')[0], 'value', 'foo'); 60 | assert.propertyVal(lex('+')[0], 'value', '+'); 61 | assert.propertyVal(lex('%')[0], 'value', '%'); 62 | assert.propertyVal(lex('(')[0], 'value', '('); 63 | // and on and on... 64 | }); 65 | }); 66 | describe('multi-symbol expressions', function() { 67 | it('should return an empty list for an empty expression', function() { 68 | assert.lengthOf(lex(''), 0, 'empty expressions are empty'); 69 | }); 70 | it('should return one symbol', function() { 71 | assert.lengthOf(lex(' '), 1, 'space is one symbol'); 72 | assert.lengthOf(lex(' '), 1, 'spaces are grouped'); 73 | assert.lengthOf(lex('a'), 1, 'a name is one symbol'); 74 | assert.lengthOf(lex('foo'), 1, 'a name chan have multiple chars'); 75 | }); 76 | it('should return many symbols', function() { 77 | assert.lengthOf(lex('a b'), 3); 78 | assert.lengthOf(lex('a + b'), 5); 79 | assert.lengthOf(lex('a+b'), 3); 80 | assert.lengthOf(lex('2a'), 2, 'leading numbers are not part of a name'); 81 | assert.lengthOf(lex('a2'), 1, 'names can have numbers after the first character'); 82 | assert.lengthOf(lex('a2b'), 1, 'letters can follow numbers in names'); 83 | assert.lengthOf(lex('a+'), 2, 'operators are not part of names'); 84 | assert.lengthOf(lex('(1)'), 3); 85 | assert.lengthOf(lex('f()'), 3); 86 | assert.lengthOf(lex('.5'), 1, 'decimal numbers are one symbol'); 87 | assert.lengthOf(lex('1.5'), 1, 'decimal numbers are one symbol'); 88 | assert.lengthOf(lex('1.5.5'), 2, 'one number cannot have two dots.'); 89 | }); 90 | }); 91 | describe('data structure', function() { 92 | it('should be defined for empty expressions', function() { 93 | assert.deepEqual(lex(''), [], 'empty expression -> empty list'); 94 | }); 95 | it('should otherswise be an array of objects', function() { 96 | assert.deepEqual(lex(' '), [{type: 'token', token: 'space', value: ' ', repr: ' '}]); 97 | assert.deepEqual( 98 | lex('2 + sin(t)'), 99 | [{type: 'token', token: 'literal', value: '2', repr: '2'}, 100 | {type: 'token', token: 'space', value: ' ', repr: ' '}, 101 | {type: 'token', token: 'operator', value: '+', repr: '+'}, 102 | {type: 'token', token: 'space', value: ' ', repr: ' '}, 103 | {type: 'token', token: 'name', value: 'sin', repr: 'sin'}, 104 | {type: 'token', token: 'paren', value: '(', repr: '('}, 105 | {type: 'token', token: 'name', value: 't', repr: 't'}, 106 | {type: 'token', token: 'paren', value: ')', repr: ')'}]); 107 | }); 108 | }); 109 | describe('invalid tokens', function() { 110 | it('should give 1-char tokens of token: "null"', function() { 111 | assert.deepEqual(lex('!'), [{type: 'token', token: null, value: '!', repr: '!'}]); 112 | assert.deepEqual( 113 | lex('!@&'), 114 | [{type: 'token', token: null, value: '!', repr: '!'}, 115 | {type: 'token', token: null, value: '@', repr: '@'}, 116 | {type: 'token', token: null, value: '&', repr: '&'}]); 117 | assert.deepEqual( 118 | lex('abc!def'), 119 | [{type: 'token', token: 'name', value: 'abc', repr: 'abc'}, 120 | {type: 'token', token: null, value: '!', repr: '!'}, 121 | {type: 'token', token: 'name', value: 'def', repr: 'def'}]); 122 | }); 123 | }); 124 | describe('creates valid tokens', function() { 125 | it('should always have type: "token"', function() { 126 | assert.propertyVal(lex(' ')[0], 'type', 'token'); 127 | assert.propertyVal(lex('0')[0], 'type', 'token'); 128 | assert.propertyVal(lex('1.5')[0], 'type', 'token'); 129 | assert.propertyVal(lex('a')[0], 'type', 'token'); 130 | assert.propertyVal(lex('+')[0], 'type', 'token'); 131 | assert.propertyVal(lex('-')[0], 'type', 'token'); 132 | assert.propertyVal(lex('%')[0], 'type', 'token'); 133 | assert.propertyVal(lex('*')[0], 'type', 'token'); 134 | assert.propertyVal(lex('/')[0], 'type', 'token'); 135 | assert.propertyVal(lex('^')[0], 'type', 'token'); 136 | assert.propertyVal(lex('<')[0], 'type', 'token'); 137 | assert.propertyVal(lex('>')[0], 'type', 'token'); 138 | assert.propertyVal(lex('(')[0], 'type', 'token'); 139 | assert.propertyVal(lex(')')[0], 'type', 'token'); 140 | }); 141 | it('should be an array of all type:"token"', function() { 142 | lex('1 + 1').map(function(token) { 143 | assert.propertyVal(token, 'type', 'token'); 144 | }); 145 | }); 146 | }); 147 | }); 148 | 149 | 150 | describe('Parser', function() { 151 | describe('simple cases', function() { 152 | it('should parse a simple literal value', function() { 153 | assert.equal(parse('1').children[0].node, 'literal'); 154 | }); 155 | it('should parse a simple name', function() { 156 | assert.equal(parse('a').children[0].node, 'name'); 157 | }); 158 | it('should parse a function call', function() { 159 | assert.equal(parse('f(x)').children[0].node, 'func'); 160 | }); 161 | it('should parse operators', function() { 162 | assert.equal(parse('-1').children[0].node, 'func'); 163 | assert.equal(parse('1-2').children[0].node, 'func'); 164 | assert.equal(parse('1+2').children[0].node, 'func'); 165 | assert.equal(parse('1%2').children[0].node, 'func'); 166 | assert.equal(parse('1*2').children[0].node, 'func'); 167 | assert.equal(parse('1/2').children[0].node, 'func'); 168 | assert.equal(parse('1^2').children[0].node, 'func'); 169 | }); 170 | it('should puke on invalid operators', function() { 171 | assert.throws(function() { parse('$'); }, parse.ParseError); 172 | }); 173 | it('should not die on whitespace', function() { 174 | assert.doesNotThrow(function() { parse(' 1'); }, parse.ParseError); 175 | assert.doesNotThrow(function() { parse('1 '); }, parse.ParseError); 176 | assert.doesNotThrow(function() { parse('1 + 1'); }, parse.ParseError); 177 | }); 178 | }); 179 | describe('parens', function() { 180 | it('should complain about mismatched parens', function() { 181 | assert.throws(function() { parse('('); }, parse.ParseError); 182 | assert.throws(function() { parse(')'); }, parse.ParseError); 183 | assert.throws(function() { parse('(()'); }, parse.ParseError); 184 | assert.throws(function() { parse('())'); }, parse.ParseError); 185 | }); 186 | it('should parse contents as a subexpression', function() { 187 | assert.equal(parse('(1)').children[0].node, 'expr'); 188 | assert.equal(parse('(1)').children[0].children[0].node, 'literal'); 189 | }); 190 | it('should put function expressions as children', function() { 191 | assert.equal(parse('sin(t)').children[0].node, 'func'); 192 | assert.equal(parse('sin(t)').children[0].children[0].node, 'name'); 193 | }); 194 | it('should reject brackets', function() { 195 | assert.throws(function() { parse('[1]'); }, parse.ParseError); 196 | }); 197 | }); 198 | describe('operators', function() { 199 | it('should error for trailing operators', function() { 200 | assert.throws(function() { parse('1+'); }, parse.ParseError); 201 | }); 202 | it('should error for leading non-unary operators', function() { 203 | assert.throws(function() { parse('*1'); }, parse.ParseError); 204 | }); 205 | }); 206 | describe('plus', function() { 207 | it('should pull minus into plus unary-minus', function() { 208 | assert.equal(parse('1-2').children[0].node, 'func'); 209 | assert.equal(parse('1-2').children[0].children[1].node, 'func'); 210 | }); 211 | it('should pull unary minuses together', function() { 212 | assert.equal(parse('-1').children[0].node, 'func'); 213 | assert.equal(parse('--1').children[0].node, 'func'); 214 | assert.equal(parse('--1').children[0].children[0].node, 'func'); 215 | assert.equal(parse('1--2').children[0].node, 'func'); 216 | assert.equal(parse('1--2').children[0].children[1].node, 'func'); 217 | }); 218 | it('should group plusses', function() { 219 | assert.equal(parse('1+2+3').children[0].children.length, 3); 220 | assert.equal(parse('1+2+3').children[0].children[0].options.value, 1); 221 | assert.equal(parse('1+2+3').children[0].children[1].options.value, 2); 222 | assert.equal(parse('1+2+3').children[0].children[2].options.value, 3); 223 | assert.equal(parse('1+2-3').children[0].children.length, 3); 224 | assert.equal(parse('1-2+3').children[0].children.length, 3); 225 | assert.equal(parse('1-2-3').children[0].children.length, 3); 226 | assert.equal(parse('1-2-3').children[0].options.key, 'sum'); 227 | }); 228 | }); 229 | describe('templating', function() { 230 | it('should attach spaces to the lower-precedent neighbour', function() { 231 | assert.equal(parse('1 + 1').children[0].template, '# + #'); 232 | }); 233 | }); 234 | describe('ParseError', function() { 235 | it('should break if instantiated without "new" operator', function() { 236 | assert.throws(function() { parse.ParseError(); }, Error); 237 | }); 238 | it('should stringify to the provided message', function() { 239 | assert.equal('' + (new parse.ParseError('abc')), 'abc'); 240 | }); 241 | }); 242 | describe('node factory', function() { 243 | it('should create valid nodes with only a node name', function() { 244 | var node = parse.astNode('a'); 245 | assert.equal(node.node, 'a'); 246 | assert.deepEqual(node.children, []); 247 | assert.deepEqual(node.options, {}); 248 | }); 249 | }); 250 | describe('regressions', function() { 251 | it('should work for 1^2-3', function() { 252 | assert.doesNotThrow(function() { parse('1^2-3'); }, parse.ParseError); 253 | }); 254 | it('should work for 1--2', function() { 255 | assert.doesNotThrow(function() { parse('1--2'); }, parse.ParseError); 256 | }); 257 | it('should work for unary negation after operators like 1+-1', function() { 258 | assert.doesNotThrow(function() { parse('1+-1'); }, parse.ParseError); 259 | }); 260 | it('should error on dangling close paren', function() { 261 | assert.throws(function() { parse('1)'); }, parse.ParseError); 262 | }); 263 | it('should error on dangling open paren', function() { 264 | assert.throws(function() { parse('1('); }, parse.ParseError); 265 | }); 266 | it('should not die for expressions with no value', function() { 267 | assert.doesNotThrow(function() { parse(''); }, parse.ParseError); 268 | }); 269 | it('should not die for expressions with 2 or more values', function() { 270 | assert.doesNotThrow(function() { parse('1 1'); }, parse.ParseError); 271 | assert.doesNotThrow(function() { parse('1 1 1'); }, parse.ParseError); 272 | }); 273 | it('should throw properly for "-#" (from fuzzer)', function() { 274 | assert.throws(function() { parse('-#'); }, parse.ParseError); 275 | }); 276 | it('should not die for a whitespace expression (fuzzer)', function() { 277 | assert.doesNotThrow(function() { parse(' '); }); 278 | }); 279 | it('should not die for weird fuzzer discovery: "a>2')(), 0); 434 | /* istanbul ignore next */ 435 | assert.equal(compileF('1>0')(), 1); 436 | /* istanbul ignore next */ 437 | assert.equal(compileF('3%2')(), 1); 438 | /* istanbul ignore next */ 439 | assert.equal(compileF('PI')(), Math.PI); 440 | /* istanbul ignore next */ 441 | assert.closeTo(compileF('sin(PI)')(), 0, eps); 442 | /* istanbul ignore next */ 443 | assert.closeTo(compileF('cos(PI)')(), -1, eps); 444 | }); 445 | it('should handle the minus operator', function() { 446 | assert.equal(compileF('-1')(), -1); 447 | }); 448 | it('should handle explicit floats', function() { 449 | assert.equal(compileF('0.5')(), 0.5); 450 | }); 451 | }); 452 | describe('regressions', function() { 453 | it('should not be sensitive to casing of constants in Math', function() { 454 | assert.equal(compileF('e')(), Math.E); 455 | /* istanbul ignore next */ 456 | assert.equal(compileF('pi')(), Math.PI); 457 | /* istanbul ignore next */ 458 | assert.equal(compileF('pI')(), Math.PI); 459 | /* istanbul ignore next */ 460 | assert.equal(compileF('Pi')(), Math.PI); 461 | /* istanbul ignore next */ 462 | assert.equal(compileF('PI')(), Math.PI); 463 | }); 464 | }); 465 | }); 466 | 467 | 468 | describe('Value compiler', function() { 469 | describe('top-level solutions', function() { 470 | it('should agree with the plain function compiler', function() { 471 | assert.equal(compileV('1')()[0], compileF('1')()); 472 | }); 473 | it('should have a value for each AST node', function() { 474 | assert.equal(compileV('1')().length, 2); 475 | assert.equal(compileV('1+1')().length, 4); 476 | }); 477 | }); 478 | }); 479 | -------------------------------------------------------------------------------- /values.js: -------------------------------------------------------------------------------- 1 | /* Get a function yielding an array of computed values for each node in the 2 | * AST, indexed by AST node id */ 3 | 4 | var R = require('ramda'); 5 | var parse = require('./parse'); 6 | var astFunc = require('./func').fromAST; 7 | 8 | 9 | var _values = function(AST) { 10 | var funcs = []; 11 | (function collectFuncs(node) { 12 | funcs[node.id] = astFunc(node); 13 | node.children.forEach(collectFuncs); 14 | })(AST); 15 | return function(ctx) { 16 | return funcs.map(function(func) { 17 | return func(ctx); 18 | }); 19 | }; 20 | }; 21 | 22 | 23 | var values = R.pipe(parse, _values); 24 | values.fromAST = _values; 25 | module.exports = values; 26 | --------------------------------------------------------------------------------