UNPKG

expr-eval

Version:
1,796 lines (1,611 loc) 51.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.exprEval = {})); }(this, function (exports) { 'use strict'; var INUMBER = 'INUMBER'; var IOP1 = 'IOP1'; var IOP2 = 'IOP2'; var IOP3 = 'IOP3'; var IVAR = 'IVAR'; var IVARNAME = 'IVARNAME'; var IFUNCALL = 'IFUNCALL'; var IFUNDEF = 'IFUNDEF'; var IEXPR = 'IEXPR'; var IEXPREVAL = 'IEXPREVAL'; var IMEMBER = 'IMEMBER'; var IENDSTATEMENT = 'IENDSTATEMENT'; var IARRAY = 'IARRAY'; function Instruction(type, value) { this.type = type; this.value = (value !== undefined && value !== null) ? value : 0; } Instruction.prototype.toString = function () { switch (this.type) { case INUMBER: case IOP1: case IOP2: case IOP3: case IVAR: case IVARNAME: case IENDSTATEMENT: return this.value; case IFUNCALL: return 'CALL ' + this.value; case IFUNDEF: return 'DEF ' + this.value; case IARRAY: return 'ARRAY ' + this.value; case IMEMBER: return '.' + this.value; default: return 'Invalid Instruction'; } }; function unaryInstruction(value) { return new Instruction(IOP1, value); } function binaryInstruction(value) { return new Instruction(IOP2, value); } function ternaryInstruction(value) { return new Instruction(IOP3, value); } function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) { var nstack = []; var newexpression = []; var n1, n2, n3; var f; for (var i = 0; i < tokens.length; i++) { var item = tokens[i]; var type = item.type; if (type === INUMBER || type === IVARNAME) { if (Array.isArray(item.value)) { nstack.push.apply(nstack, simplify(item.value.map(function (x) { return new Instruction(INUMBER, x); }).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values)); } else { nstack.push(item); } } else if (type === IVAR && values.hasOwnProperty(item.value)) { item = new Instruction(INUMBER, values[item.value]); nstack.push(item); } else if (type === IOP2 && nstack.length > 1) { n2 = nstack.pop(); n1 = nstack.pop(); f = binaryOps[item.value]; item = new Instruction(INUMBER, f(n1.value, n2.value)); nstack.push(item); } else if (type === IOP3 && nstack.length > 2) { n3 = nstack.pop(); n2 = nstack.pop(); n1 = nstack.pop(); if (item.value === '?') { nstack.push(n1.value ? n2.value : n3.value); } else { f = ternaryOps[item.value]; item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value)); nstack.push(item); } } else if (type === IOP1 && nstack.length > 0) { n1 = nstack.pop(); f = unaryOps[item.value]; item = new Instruction(INUMBER, f(n1.value)); nstack.push(item); } else if (type === IEXPR) { while (nstack.length > 0) { newexpression.push(nstack.shift()); } newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values))); } else if (type === IMEMBER && nstack.length > 0) { n1 = nstack.pop(); nstack.push(new Instruction(INUMBER, n1.value[item.value])); } /* else if (type === IARRAY && nstack.length >= item.value) { var length = item.value; while (length-- > 0) { newexpression.push(nstack.pop()); } newexpression.push(new Instruction(IARRAY, item.value)); } */ else { while (nstack.length > 0) { newexpression.push(nstack.shift()); } newexpression.push(item); } } while (nstack.length > 0) { newexpression.push(nstack.shift()); } return newexpression; } function substitute(tokens, variable, expr) { var newexpression = []; for (var i = 0; i < tokens.length; i++) { var item = tokens[i]; var type = item.type; if (type === IVAR && item.value === variable) { for (var j = 0; j < expr.tokens.length; j++) { var expritem = expr.tokens[j]; var replitem; if (expritem.type === IOP1) { replitem = unaryInstruction(expritem.value); } else if (expritem.type === IOP2) { replitem = binaryInstruction(expritem.value); } else if (expritem.type === IOP3) { replitem = ternaryInstruction(expritem.value); } else { replitem = new Instruction(expritem.type, expritem.value); } newexpression.push(replitem); } } else if (type === IEXPR) { newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr))); } else { newexpression.push(item); } } return newexpression; } function evaluate(tokens, expr, values) { var nstack = []; var n1, n2, n3; var f, args, argCount; if (isExpressionEvaluator(tokens)) { return resolveExpression(tokens, values); } var numTokens = tokens.length; for (var i = 0; i < numTokens; i++) { var item = tokens[i]; var type = item.type; if (type === INUMBER || type === IVARNAME) { nstack.push(item.value); } else if (type === IOP2) { n2 = nstack.pop(); n1 = nstack.pop(); if (item.value === 'and') { nstack.push(n1 ? !!evaluate(n2, expr, values) : false); } else if (item.value === 'or') { nstack.push(n1 ? true : !!evaluate(n2, expr, values)); } else if (item.value === '=') { f = expr.binaryOps[item.value]; nstack.push(f(n1, evaluate(n2, expr, values), values)); } else { f = expr.binaryOps[item.value]; nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values))); } } else if (type === IOP3) { n3 = nstack.pop(); n2 = nstack.pop(); n1 = nstack.pop(); if (item.value === '?') { nstack.push(evaluate(n1 ? n2 : n3, expr, values)); } else { f = expr.ternaryOps[item.value]; nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values))); } } else if (type === IVAR) { if (item.value in expr.functions) { nstack.push(expr.functions[item.value]); } else if (item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) { nstack.push(expr.unaryOps[item.value]); } else { var v = values[item.value]; if (v !== undefined) { nstack.push(v); } else { throw new Error('undefined variable: ' + item.value); } } } else if (type === IOP1) { n1 = nstack.pop(); f = expr.unaryOps[item.value]; nstack.push(f(resolveExpression(n1, values))); } else if (type === IFUNCALL) { argCount = item.value; args = []; while (argCount-- > 0) { args.unshift(resolveExpression(nstack.pop(), values)); } f = nstack.pop(); if (f.apply && f.call) { nstack.push(f.apply(undefined, args)); } else { throw new Error(f + ' is not a function'); } } else if (type === IFUNDEF) { // Create closure to keep references to arguments and expression nstack.push((function () { var n2 = nstack.pop(); var args = []; var argCount = item.value; while (argCount-- > 0) { args.unshift(nstack.pop()); } var n1 = nstack.pop(); var f = function () { var scope = Object.assign({}, values); for (var i = 0, len = args.length; i < len; i++) { scope[args[i]] = arguments[i]; } return evaluate(n2, expr, scope); }; // f.name = n1 Object.defineProperty(f, 'name', { value: n1, writable: false }); values[n1] = f; return f; })()); } else if (type === IEXPR) { nstack.push(createExpressionEvaluator(item, expr)); } else if (type === IEXPREVAL) { nstack.push(item); } else if (type === IMEMBER) { n1 = nstack.pop(); nstack.push(n1[item.value]); } else if (type === IENDSTATEMENT) { nstack.pop(); } else if (type === IARRAY) { argCount = item.value; args = []; while (argCount-- > 0) { args.unshift(nstack.pop()); } nstack.push(args); } else { throw new Error('invalid Expression'); } } if (nstack.length > 1) { throw new Error('invalid Expression (parity)'); } // Explicitly return zero to avoid test issues caused by -0 return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values); } function createExpressionEvaluator(token, expr, values) { if (isExpressionEvaluator(token)) return token; return { type: IEXPREVAL, value: function (scope) { return evaluate(token.value, expr, scope); } }; } function isExpressionEvaluator(n) { return n && n.type === IEXPREVAL; } function resolveExpression(n, values) { return isExpressionEvaluator(n) ? n.value(values) : n; } function expressionToString(tokens, toJS) { var nstack = []; var n1, n2, n3; var f, args, argCount; for (var i = 0; i < tokens.length; i++) { var item = tokens[i]; var type = item.type; if (type === INUMBER) { if (typeof item.value === 'number' && item.value < 0) { nstack.push('(' + item.value + ')'); } else if (Array.isArray(item.value)) { nstack.push('[' + item.value.map(escapeValue).join(', ') + ']'); } else { nstack.push(escapeValue(item.value)); } } else if (type === IOP2) { n2 = nstack.pop(); n1 = nstack.pop(); f = item.value; if (toJS) { if (f === '^') { nstack.push('Math.pow(' + n1 + ', ' + n2 + ')'); } else if (f === 'and') { nstack.push('(!!' + n1 + ' && !!' + n2 + ')'); } else if (f === 'or') { nstack.push('(!!' + n1 + ' || !!' + n2 + ')'); } else if (f === '||') { nstack.push('(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((' + n1 + '),(' + n2 + ')))'); } else if (f === '==') { nstack.push('(' + n1 + ' === ' + n2 + ')'); } else if (f === '!=') { nstack.push('(' + n1 + ' !== ' + n2 + ')'); } else if (f === '[') { nstack.push(n1 + '[(' + n2 + ') | 0]'); } else { nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); } } else { if (f === '[') { nstack.push(n1 + '[' + n2 + ']'); } else { nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); } } } else if (type === IOP3) { n3 = nstack.pop(); n2 = nstack.pop(); n1 = nstack.pop(); f = item.value; if (f === '?') { nstack.push('(' + n1 + ' ? ' + n2 + ' : ' + n3 + ')'); } else { throw new Error('invalid Expression'); } } else if (type === IVAR || type === IVARNAME) { nstack.push(item.value); } else if (type === IOP1) { n1 = nstack.pop(); f = item.value; if (f === '-' || f === '+') { nstack.push('(' + f + n1 + ')'); } else if (toJS) { if (f === 'not') { nstack.push('(' + '!' + n1 + ')'); } else if (f === '!') { nstack.push('fac(' + n1 + ')'); } else { nstack.push(f + '(' + n1 + ')'); } } else if (f === '!') { nstack.push('(' + n1 + '!)'); } else { nstack.push('(' + f + ' ' + n1 + ')'); } } else if (type === IFUNCALL) { argCount = item.value; args = []; while (argCount-- > 0) { args.unshift(nstack.pop()); } f = nstack.pop(); nstack.push(f + '(' + args.join(', ') + ')'); } else if (type === IFUNDEF) { n2 = nstack.pop(); argCount = item.value; args = []; while (argCount-- > 0) { args.unshift(nstack.pop()); } n1 = nstack.pop(); if (toJS) { nstack.push('(' + n1 + ' = function(' + args.join(', ') + ') { return ' + n2 + ' })'); } else { nstack.push('(' + n1 + '(' + args.join(', ') + ') = ' + n2 + ')'); } } else if (type === IMEMBER) { n1 = nstack.pop(); nstack.push(n1 + '.' + item.value); } else if (type === IARRAY) { argCount = item.value; args = []; while (argCount-- > 0) { args.unshift(nstack.pop()); } nstack.push('[' + args.join(', ') + ']'); } else if (type === IEXPR) { nstack.push('(' + expressionToString(item.value, toJS) + ')'); } else if (type === IENDSTATEMENT) ; else { throw new Error('invalid Expression'); } } if (nstack.length > 1) { if (toJS) { nstack = [ nstack.join(',') ]; } else { nstack = [ nstack.join(';') ]; } } return String(nstack[0]); } function escapeValue(v) { if (typeof v === 'string') { return JSON.stringify(v).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); } return v; } function contains(array, obj) { for (var i = 0; i < array.length; i++) { if (array[i] === obj) { return true; } } return false; } function getSymbols(tokens, symbols, options) { options = options || {}; var withMembers = !!options.withMembers; var prevVar = null; for (var i = 0; i < tokens.length; i++) { var item = tokens[i]; if (item.type === IVAR || item.type === IVARNAME) { if (!withMembers && !contains(symbols, item.value)) { symbols.push(item.value); } else if (prevVar !== null) { if (!contains(symbols, prevVar)) { symbols.push(prevVar); } prevVar = item.value; } else { prevVar = item.value; } } else if (item.type === IMEMBER && withMembers && prevVar !== null) { prevVar += '.' + item.value; } else if (item.type === IEXPR) { getSymbols(item.value, symbols, options); } else if (prevVar !== null) { if (!contains(symbols, prevVar)) { symbols.push(prevVar); } prevVar = null; } } if (prevVar !== null && !contains(symbols, prevVar)) { symbols.push(prevVar); } } function Expression(tokens, parser) { this.tokens = tokens; this.parser = parser; this.unaryOps = parser.unaryOps; this.binaryOps = parser.binaryOps; this.ternaryOps = parser.ternaryOps; this.functions = parser.functions; } Expression.prototype.simplify = function (values) { values = values || {}; return new Expression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser); }; Expression.prototype.substitute = function (variable, expr) { if (!(expr instanceof Expression)) { expr = this.parser.parse(String(expr)); } return new Expression(substitute(this.tokens, variable, expr), this.parser); }; Expression.prototype.evaluate = function (values) { values = values || {}; return evaluate(this.tokens, this, values); }; Expression.prototype.toString = function () { return expressionToString(this.tokens, false); }; Expression.prototype.symbols = function (options) { options = options || {}; var vars = []; getSymbols(this.tokens, vars, options); return vars; }; Expression.prototype.variables = function (options) { options = options || {}; var vars = []; getSymbols(this.tokens, vars, options); var functions = this.functions; return vars.filter(function (name) { return !(name in functions); }); }; Expression.prototype.toJSFunction = function (param, variables) { var expr = this; var f = new Function(param, 'with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return ' + expressionToString(this.simplify(variables).tokens, true) + '; }'); // eslint-disable-line no-new-func return function () { return f.apply(expr, arguments); }; }; var TEOF = 'TEOF'; var TOP = 'TOP'; var TNUMBER = 'TNUMBER'; var TSTRING = 'TSTRING'; var TPAREN = 'TPAREN'; var TBRACKET = 'TBRACKET'; var TCOMMA = 'TCOMMA'; var TNAME = 'TNAME'; var TSEMICOLON = 'TSEMICOLON'; function Token(type, value, index) { this.type = type; this.value = value; this.index = index; } Token.prototype.toString = function () { return this.type + ': ' + this.value; }; function TokenStream(parser, expression) { this.pos = 0; this.current = null; this.unaryOps = parser.unaryOps; this.binaryOps = parser.binaryOps; this.ternaryOps = parser.ternaryOps; this.consts = parser.consts; this.expression = expression; this.savedPosition = 0; this.savedCurrent = null; this.options = parser.options; this.parser = parser; } TokenStream.prototype.newToken = function (type, value, pos) { return new Token(type, value, pos != null ? pos : this.pos); }; TokenStream.prototype.save = function () { this.savedPosition = this.pos; this.savedCurrent = this.current; }; TokenStream.prototype.restore = function () { this.pos = this.savedPosition; this.current = this.savedCurrent; }; TokenStream.prototype.next = function () { if (this.pos >= this.expression.length) { return this.newToken(TEOF, 'EOF'); } if (this.isWhitespace() || this.isComment()) { return this.next(); } else if (this.isRadixInteger() || this.isNumber() || this.isOperator() || this.isString() || this.isParen() || this.isBracket() || this.isComma() || this.isSemicolon() || this.isNamedOp() || this.isConst() || this.isName()) { return this.current; } else { this.parseError('Unknown character "' + this.expression.charAt(this.pos) + '"'); } }; TokenStream.prototype.isString = function () { var r = false; var startPos = this.pos; var quote = this.expression.charAt(startPos); if (quote === '\'' || quote === '"') { var index = this.expression.indexOf(quote, startPos + 1); while (index >= 0 && this.pos < this.expression.length) { this.pos = index + 1; if (this.expression.charAt(index - 1) !== '\\') { var rawString = this.expression.substring(startPos + 1, index); this.current = this.newToken(TSTRING, this.unescape(rawString), startPos); r = true; break; } index = this.expression.indexOf(quote, index + 1); } } return r; }; TokenStream.prototype.isParen = function () { var c = this.expression.charAt(this.pos); if (c === '(' || c === ')') { this.current = this.newToken(TPAREN, c); this.pos++; return true; } return false; }; TokenStream.prototype.isBracket = function () { var c = this.expression.charAt(this.pos); if ((c === '[' || c === ']') && this.isOperatorEnabled('[')) { this.current = this.newToken(TBRACKET, c); this.pos++; return true; } return false; }; TokenStream.prototype.isComma = function () { var c = this.expression.charAt(this.pos); if (c === ',') { this.current = this.newToken(TCOMMA, ','); this.pos++; return true; } return false; }; TokenStream.prototype.isSemicolon = function () { var c = this.expression.charAt(this.pos); if (c === ';') { this.current = this.newToken(TSEMICOLON, ';'); this.pos++; return true; } return false; }; TokenStream.prototype.isConst = function () { var startPos = this.pos; var i = startPos; for (; i < this.expression.length; i++) { var c = this.expression.charAt(i); if (c.toUpperCase() === c.toLowerCase()) { if (i === this.pos || (c !== '_' && c !== '.' && (c < '0' || c > '9'))) { break; } } } if (i > startPos) { var str = this.expression.substring(startPos, i); if (str in this.consts) { this.current = this.newToken(TNUMBER, this.consts[str]); this.pos += str.length; return true; } } return false; }; TokenStream.prototype.isNamedOp = function () { var startPos = this.pos; var i = startPos; for (; i < this.expression.length; i++) { var c = this.expression.charAt(i); if (c.toUpperCase() === c.toLowerCase()) { if (i === this.pos || (c !== '_' && (c < '0' || c > '9'))) { break; } } } if (i > startPos) { var str = this.expression.substring(startPos, i); if (this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) { this.current = this.newToken(TOP, str); this.pos += str.length; return true; } } return false; }; TokenStream.prototype.isName = function () { var startPos = this.pos; var i = startPos; var hasLetter = false; for (; i < this.expression.length; i++) { var c = this.expression.charAt(i); if (c.toUpperCase() === c.toLowerCase()) { if (i === this.pos && (c === '$' || c === '_')) { if (c === '_') { hasLetter = true; } continue; } else if (i === this.pos || !hasLetter || (c !== '_' && (c < '0' || c > '9'))) { break; } } else { hasLetter = true; } } if (hasLetter) { var str = this.expression.substring(startPos, i); this.current = this.newToken(TNAME, str); this.pos += str.length; return true; } return false; }; TokenStream.prototype.isWhitespace = function () { var r = false; var c = this.expression.charAt(this.pos); while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { r = true; this.pos++; if (this.pos >= this.expression.length) { break; } c = this.expression.charAt(this.pos); } return r; }; var codePointPattern = /^[0-9a-f]{4}$/i; TokenStream.prototype.unescape = function (v) { var index = v.indexOf('\\'); if (index < 0) { return v; } var buffer = v.substring(0, index); while (index >= 0) { var c = v.charAt(++index); switch (c) { case '\'': buffer += '\''; break; case '"': buffer += '"'; break; case '\\': buffer += '\\'; break; case '/': buffer += '/'; break; case 'b': buffer += '\b'; break; case 'f': buffer += '\f'; break; case 'n': buffer += '\n'; break; case 'r': buffer += '\r'; break; case 't': buffer += '\t'; break; case 'u': // interpret the following 4 characters as the hex of the unicode code point var codePoint = v.substring(index + 1, index + 5); if (!codePointPattern.test(codePoint)) { this.parseError('Illegal escape sequence: \\u' + codePoint); } buffer += String.fromCharCode(parseInt(codePoint, 16)); index += 4; break; default: throw this.parseError('Illegal escape sequence: "\\' + c + '"'); } ++index; var backslash = v.indexOf('\\', index); buffer += v.substring(index, backslash < 0 ? v.length : backslash); index = backslash; } return buffer; }; TokenStream.prototype.isComment = function () { var c = this.expression.charAt(this.pos); if (c === '/' && this.expression.charAt(this.pos + 1) === '*') { this.pos = this.expression.indexOf('*/', this.pos) + 2; if (this.pos === 1) { this.pos = this.expression.length; } return true; } return false; }; TokenStream.prototype.isRadixInteger = function () { var pos = this.pos; if (pos >= this.expression.length - 2 || this.expression.charAt(pos) !== '0') { return false; } ++pos; var radix; var validDigit; if (this.expression.charAt(pos) === 'x') { radix = 16; validDigit = /^[0-9a-f]$/i; ++pos; } else if (this.expression.charAt(pos) === 'b') { radix = 2; validDigit = /^[01]$/i; ++pos; } else { return false; } var valid = false; var startPos = pos; while (pos < this.expression.length) { var c = this.expression.charAt(pos); if (validDigit.test(c)) { pos++; valid = true; } else { break; } } if (valid) { this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix)); this.pos = pos; } return valid; }; TokenStream.prototype.isNumber = function () { var valid = false; var pos = this.pos; var startPos = pos; var resetPos = pos; var foundDot = false; var foundDigits = false; var c; while (pos < this.expression.length) { c = this.expression.charAt(pos); if ((c >= '0' && c <= '9') || (!foundDot && c === '.')) { if (c === '.') { foundDot = true; } else { foundDigits = true; } pos++; valid = foundDigits; } else { break; } } if (valid) { resetPos = pos; } if (c === 'e' || c === 'E') { pos++; var acceptSign = true; var validExponent = false; while (pos < this.expression.length) { c = this.expression.charAt(pos); if (acceptSign && (c === '+' || c === '-')) { acceptSign = false; } else if (c >= '0' && c <= '9') { validExponent = true; acceptSign = false; } else { break; } pos++; } if (!validExponent) { pos = resetPos; } } if (valid) { this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos))); this.pos = pos; } else { this.pos = resetPos; } return valid; }; TokenStream.prototype.isOperator = function () { var startPos = this.pos; var c = this.expression.charAt(this.pos); if (c === '+' || c === '-' || c === '*' || c === '/' || c === '%' || c === '^' || c === '?' || c === ':' || c === '.') { this.current = this.newToken(TOP, c); } else if (c === '∙' || c === '•') { this.current = this.newToken(TOP, '*'); } else if (c === '>') { if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(TOP, '>='); this.pos++; } else { this.current = this.newToken(TOP, '>'); } } else if (c === '<') { if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(TOP, '<='); this.pos++; } else { this.current = this.newToken(TOP, '<'); } } else if (c === '|') { if (this.expression.charAt(this.pos + 1) === '|') { this.current = this.newToken(TOP, '||'); this.pos++; } else { return false; } } else if (c === '=') { if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(TOP, '=='); this.pos++; } else { this.current = this.newToken(TOP, c); } } else if (c === '!') { if (this.expression.charAt(this.pos + 1) === '=') { this.current = this.newToken(TOP, '!='); this.pos++; } else { this.current = this.newToken(TOP, c); } } else { return false; } this.pos++; if (this.isOperatorEnabled(this.current.value)) { return true; } else { this.pos = startPos; return false; } }; TokenStream.prototype.isOperatorEnabled = function (op) { return this.parser.isOperatorEnabled(op); }; TokenStream.prototype.getCoordinates = function () { var line = 0; var column; var newline = -1; do { line++; column = this.pos - newline; newline = this.expression.indexOf('\n', newline + 1); } while (newline >= 0 && newline < this.pos); return { line: line, column: column }; }; TokenStream.prototype.parseError = function (msg) { var coords = this.getCoordinates(); throw new Error('parse error [' + coords.line + ':' + coords.column + ']: ' + msg); }; function ParserState(parser, tokenStream, options) { this.parser = parser; this.tokens = tokenStream; this.current = null; this.nextToken = null; this.next(); this.savedCurrent = null; this.savedNextToken = null; this.allowMemberAccess = options.allowMemberAccess !== false; } ParserState.prototype.next = function () { this.current = this.nextToken; return (this.nextToken = this.tokens.next()); }; ParserState.prototype.tokenMatches = function (token, value) { if (typeof value === 'undefined') { return true; } else if (Array.isArray(value)) { return contains(value, token.value); } else if (typeof value === 'function') { return value(token); } else { return token.value === value; } }; ParserState.prototype.save = function () { this.savedCurrent = this.current; this.savedNextToken = this.nextToken; this.tokens.save(); }; ParserState.prototype.restore = function () { this.tokens.restore(); this.current = this.savedCurrent; this.nextToken = this.savedNextToken; }; ParserState.prototype.accept = function (type, value) { if (this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) { this.next(); return true; } return false; }; ParserState.prototype.expect = function (type, value) { if (!this.accept(type, value)) { var coords = this.tokens.getCoordinates(); throw new Error('parse error [' + coords.line + ':' + coords.column + ']: Expected ' + (value || type)); } }; ParserState.prototype.parseAtom = function (instr) { var unaryOps = this.tokens.unaryOps; function isPrefixOperator(token) { return token.value in unaryOps; } if (this.accept(TNAME) || this.accept(TOP, isPrefixOperator)) { instr.push(new Instruction(IVAR, this.current.value)); } else if (this.accept(TNUMBER)) { instr.push(new Instruction(INUMBER, this.current.value)); } else if (this.accept(TSTRING)) { instr.push(new Instruction(INUMBER, this.current.value)); } else if (this.accept(TPAREN, '(')) { this.parseExpression(instr); this.expect(TPAREN, ')'); } else if (this.accept(TBRACKET, '[')) { if (this.accept(TBRACKET, ']')) { instr.push(new Instruction(IARRAY, 0)); } else { var argCount = this.parseArrayList(instr); instr.push(new Instruction(IARRAY, argCount)); } } else { throw new Error('unexpected ' + this.nextToken); } }; ParserState.prototype.parseExpression = function (instr) { var exprInstr = []; if (this.parseUntilEndStatement(instr, exprInstr)) { return; } this.parseVariableAssignmentExpression(exprInstr); if (this.parseUntilEndStatement(instr, exprInstr)) { return; } this.pushExpression(instr, exprInstr); }; ParserState.prototype.pushExpression = function (instr, exprInstr) { for (var i = 0, len = exprInstr.length; i < len; i++) { instr.push(exprInstr[i]); } }; ParserState.prototype.parseUntilEndStatement = function (instr, exprInstr) { if (!this.accept(TSEMICOLON)) return false; if (this.nextToken && this.nextToken.type !== TEOF && !(this.nextToken.type === TPAREN && this.nextToken.value === ')')) { exprInstr.push(new Instruction(IENDSTATEMENT)); } if (this.nextToken.type !== TEOF) { this.parseExpression(exprInstr); } instr.push(new Instruction(IEXPR, exprInstr)); return true; }; ParserState.prototype.parseArrayList = function (instr) { var argCount = 0; while (!this.accept(TBRACKET, ']')) { this.parseExpression(instr); ++argCount; while (this.accept(TCOMMA)) { this.parseExpression(instr); ++argCount; } } return argCount; }; ParserState.prototype.parseVariableAssignmentExpression = function (instr) { this.parseConditionalExpression(instr); while (this.accept(TOP, '=')) { var varName = instr.pop(); var varValue = []; var lastInstrIndex = instr.length - 1; if (varName.type === IFUNCALL) { if (!this.tokens.isOperatorEnabled('()=')) { throw new Error('function definition is not permitted'); } for (var i = 0, len = varName.value + 1; i < len; i++) { var index = lastInstrIndex - i; if (instr[index].type === IVAR) { instr[index] = new Instruction(IVARNAME, instr[index].value); } } this.parseVariableAssignmentExpression(varValue); instr.push(new Instruction(IEXPR, varValue)); instr.push(new Instruction(IFUNDEF, varName.value)); continue; } if (varName.type !== IVAR && varName.type !== IMEMBER) { throw new Error('expected variable for assignment'); } this.parseVariableAssignmentExpression(varValue); instr.push(new Instruction(IVARNAME, varName.value)); instr.push(new Instruction(IEXPR, varValue)); instr.push(binaryInstruction('=')); } }; ParserState.prototype.parseConditionalExpression = function (instr) { this.parseOrExpression(instr); while (this.accept(TOP, '?')) { var trueBranch = []; var falseBranch = []; this.parseConditionalExpression(trueBranch); this.expect(TOP, ':'); this.parseConditionalExpression(falseBranch); instr.push(new Instruction(IEXPR, trueBranch)); instr.push(new Instruction(IEXPR, falseBranch)); instr.push(ternaryInstruction('?')); } }; ParserState.prototype.parseOrExpression = function (instr) { this.parseAndExpression(instr); while (this.accept(TOP, 'or')) { var falseBranch = []; this.parseAndExpression(falseBranch); instr.push(new Instruction(IEXPR, falseBranch)); instr.push(binaryInstruction('or')); } }; ParserState.prototype.parseAndExpression = function (instr) { this.parseComparison(instr); while (this.accept(TOP, 'and')) { var trueBranch = []; this.parseComparison(trueBranch); instr.push(new Instruction(IEXPR, trueBranch)); instr.push(binaryInstruction('and')); } }; var COMPARISON_OPERATORS = ['==', '!=', '<', '<=', '>=', '>', 'in']; ParserState.prototype.parseComparison = function (instr) { this.parseAddSub(instr); while (this.accept(TOP, COMPARISON_OPERATORS)) { var op = this.current; this.parseAddSub(instr); instr.push(binaryInstruction(op.value)); } }; var ADD_SUB_OPERATORS = ['+', '-', '||']; ParserState.prototype.parseAddSub = function (instr) { this.parseTerm(instr); while (this.accept(TOP, ADD_SUB_OPERATORS)) { var op = this.current; this.parseTerm(instr); instr.push(binaryInstruction(op.value)); } }; var TERM_OPERATORS = ['*', '/', '%']; ParserState.prototype.parseTerm = function (instr) { this.parseFactor(instr); while (this.accept(TOP, TERM_OPERATORS)) { var op = this.current; this.parseFactor(instr); instr.push(binaryInstruction(op.value)); } }; ParserState.prototype.parseFactor = function (instr) { var unaryOps = this.tokens.unaryOps; function isPrefixOperator(token) { return token.value in unaryOps; } this.save(); if (this.accept(TOP, isPrefixOperator)) { if (this.current.value !== '-' && this.current.value !== '+') { if (this.nextToken.type === TPAREN && this.nextToken.value === '(') { this.restore(); this.parseExponential(instr); return; } else if (this.nextToken.type === TSEMICOLON || this.nextToken.type === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ')')) { this.restore(); this.parseAtom(instr); return; } } var op = this.current; this.parseFactor(instr); instr.push(unaryInstruction(op.value)); } else { this.parseExponential(instr); } }; ParserState.prototype.parseExponential = function (instr) { this.parsePostfixExpression(instr); while (this.accept(TOP, '^')) { this.parseFactor(instr); instr.push(binaryInstruction('^')); } }; ParserState.prototype.parsePostfixExpression = function (instr) { this.parseFunctionCall(instr); while (this.accept(TOP, '!')) { instr.push(unaryInstruction('!')); } }; ParserState.prototype.parseFunctionCall = function (instr) { var unaryOps = this.tokens.unaryOps; function isPrefixOperator(token) { return token.value in unaryOps; } if (this.accept(TOP, isPrefixOperator)) { var op = this.current; this.parseAtom(instr); instr.push(unaryInstruction(op.value)); } else { this.parseMemberExpression(instr); while (this.accept(TPAREN, '(')) { if (this.accept(TPAREN, ')')) { instr.push(new Instruction(IFUNCALL, 0)); } else { var argCount = this.parseArgumentList(instr); instr.push(new Instruction(IFUNCALL, argCount)); } } } }; ParserState.prototype.parseArgumentList = function (instr) { var argCount = 0; while (!this.accept(TPAREN, ')')) { this.parseExpression(instr); ++argCount; while (this.accept(TCOMMA)) { this.parseExpression(instr); ++argCount; } } return argCount; }; ParserState.prototype.parseMemberExpression = function (instr) { this.parseAtom(instr); while (this.accept(TOP, '.') || this.accept(TBRACKET, '[')) { var op = this.current; if (op.value === '.') { if (!this.allowMemberAccess) { throw new Error('unexpected ".", member access is not permitted'); } this.expect(TNAME); instr.push(new Instruction(IMEMBER, this.current.value)); } else if (op.value === '[') { if (!this.tokens.isOperatorEnabled('[')) { throw new Error('unexpected "[]", arrays are disabled'); } this.parseExpression(instr); this.expect(TBRACKET, ']'); instr.push(binaryInstruction('[')); } else { throw new Error('unexpected symbol: ' + op.value); } } }; function add(a, b) { return Number(a) + Number(b); } function sub(a, b) { return a - b; } function mul(a, b) { return a * b; } function div(a, b) { return a / b; } function mod(a, b) { return a % b; } function concat(a, b) { if (Array.isArray(a) && Array.isArray(b)) { return a.concat(b); } return '' + a + b; } function equal(a, b) { return a === b; } function notEqual(a, b) { return a !== b; } function greaterThan(a, b) { return a > b; } function lessThan(a, b) { return a < b; } function greaterThanEqual(a, b) { return a >= b; } function lessThanEqual(a, b) { return a <= b; } function andOperator(a, b) { return Boolean(a && b); } function orOperator(a, b) { return Boolean(a || b); } function inOperator(a, b) { return contains(b, a); } function sinh(a) { return ((Math.exp(a) - Math.exp(-a)) / 2); } function cosh(a) { return ((Math.exp(a) + Math.exp(-a)) / 2); } function tanh(a) { if (a === Infinity) return 1; if (a === -Infinity) return -1; return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a)); } function asinh(a) { if (a === -Infinity) return a; return Math.log(a + Math.sqrt((a * a) + 1)); } function acosh(a) { return Math.log(a + Math.sqrt((a * a) - 1)); } function atanh(a) { return (Math.log((1 + a) / (1 - a)) / 2); } function log10(a) { return Math.log(a) * Math.LOG10E; } function neg(a) { return -a; } function not(a) { return !a; } function trunc(a) { return a < 0 ? Math.ceil(a) : Math.floor(a); } function random(a) { return Math.random() * (a || 1); } function factorial(a) { // a! return gamma(a + 1); } function isInteger(value) { return isFinite(value) && (value === Math.round(value)); } var GAMMA_G = 4.7421875; var GAMMA_P = [ 0.99999999999999709182, 57.156235665862923517, -59.597960355475491248, 14.136097974741747174, -0.49191381609762019978, 0.33994649984811888699e-4, 0.46523628927048575665e-4, -0.98374475304879564677e-4, 0.15808870322491248884e-3, -0.21026444172410488319e-3, 0.21743961811521264320e-3, -0.16431810653676389022e-3, 0.84418223983852743293e-4, -0.26190838401581408670e-4, 0.36899182659531622704e-5 ]; // Gamma function from math.js function gamma(n) { var t, x; if (isInteger(n)) { if (n <= 0) { return isFinite(n) ? Infinity : NaN; } if (n > 171) { return Infinity; // Will overflow } var value = n - 2; var res = n - 1; while (value > 1) { res *= value; value--; } if (res === 0) { res = 1; // 0! is per definition 1 } return res; } if (n < 0.5) { return Math.PI / (Math.sin(Math.PI * n) * gamma(1 - n)); } if (n >= 171.35) { return Infinity; // will overflow } if (n > 85.0) { // Extended Stirling Approx var twoN = n * n; var threeN = twoN * n; var fourN = threeN * n; var fiveN = fourN * n; return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) * (1 + (1 / (12 * n)) + (1 / (288 * twoN)) - (139 / (51840 * threeN)) - (571 / (2488320 * fourN)) + (163879 / (209018880 * fiveN)) + (5246819 / (75246796800 * fiveN * n))); } --n; x = GAMMA_P[0]; for (var i = 1; i < GAMMA_P.length; ++i) { x += GAMMA_P[i] / (n + i); } t = n + GAMMA_G + 0.5; return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x; } function stringOrArrayLength(s) { if (Array.isArray(s)) { return s.length; } return String(s).length; } function hypot() { var sum = 0; var larg = 0; for (var i = 0; i < arguments.length; i++) { var arg = Math.abs(arguments[i]); var div; if (larg < arg) { div = larg / arg; sum = (sum * div * div) + 1; larg = arg; } else if (arg > 0) { div = arg / larg; sum += div * div; } else { sum += arg; } } return larg === Infinity ? Infinity : larg * Math.sqrt(sum); } function condition(cond, yep, nope) { return cond ? yep : nope; } /** * Decimal adjustment of a number. * From @escopecz. * * @param {Number} value The number. * @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). * @return {Number} The adjusted value. */ function roundTo(value, exp) { // If the exp is undefined or zero... if (typeof exp === 'undefined' || +exp === 0) { return Math.round(value); } value = +value; exp = -(+exp); // If the value is not a number or the exp is not an integer... if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { return NaN; } // Shift value = value.toString().split('e'); value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); // Shift back value = value.toString().split('e'); return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); } function setVar(name, value, variables) { if (variables) variables[name] = value; return value; } function arrayIndex(array, index) { return array[index | 0]; } function max(array) { if (arguments.length === 1 && Array.isArray(array)) { return Math.max.apply(Math, array); } else { return Math.max.apply(Math, arguments); } } function min(array) { if (arguments.length === 1 && Array.isArray(array)) { return Math.min.apply(Math, array); } else { return Math.min.apply(Math, arguments); } } function arrayMap(f, a) { if (typeof f !== 'function') { throw new Error('First argument to map is not a function'); } if (!Array.isArray(a)) { throw new Error('Second argument to map is not an array'); } return a.map(function (x, i) { return f(x, i); }); } function arrayFold(f, init, a) { if (typeof f !== 'function') { throw new Error('First argument to fold is not a function'); } if (!Array.isArray(a)) { throw new Error('Second argument to fold is not an array'); } return a.reduce(function (acc, x, i) { return f(acc, x, i); }, init); } function arrayFilter(f, a) { if (typeof f !== 'function') { throw new Error('First argument to filter is not a function'); } if (!Array.isArray(a)) { throw new Error('Second argument to filter is not an array'); } return a.filter(function (x, i) { return f(x, i); }); } function stringOrArrayIndexOf(target, s) { if (!(Array.isArray(s) || typeof s === 'string')) { throw new Error('Second argument to indexOf is not a string or array'); } return s.indexOf(target); } function arrayJoin(sep, a) { if (!Array.isArray(a)) { throw new Error('Second argument to join is not an array'); } return a.join(sep); } function sign(x) { return ((x > 0) - (x < 0)) || +x; } var ONE_THIRD = 1/3; function cbrt(x) { return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD); } function expm1(x) { return Math.exp(x) - 1; } function log1p(x) { return Math.log(1 + x); } function log2(x) { return Math.log(x) / Math.LN2; } function Parser(options) { this.options = options || {}; this.unaryOps = { sin: Math.sin, cos: Math.cos, tan: Math.tan, asin: Math.asin, acos: Math.acos, atan: Math.atan, sinh: Math.sinh || sinh, cosh: Math.cosh || cosh, tanh: Math.tanh || tanh, asinh: Math.asinh || asinh, acosh: Math.acosh || acosh, atanh: Math.atanh || atanh, sqrt: Math.sqrt, cbrt: Math.cbrt || cbrt, log: Math.log, log2: Math.log2 || log2, ln: Math.log, lg: Math.log10 || log10, log10: Math.log10 || log10, expm1: Math.expm1 || expm1, log1p: Math.log1p || log1p, abs: Math.abs, ceil: Math.ceil, floor: Math.floor, round: Math.round, trunc: Math.trunc || trunc, '-': neg, '+': Number, exp: Math.exp, not: not, length: stringOrArrayLength, '!': factorial, sign: Math.sign || sign }; this.binaryOps = { '+': add, '-': sub, '*': mul, '/': div, '%': mod, '^': Math.pow, '||': concat, '==': equal, '!=': notEqual, '>': greaterThan, '<': lessThan, '>=': greaterThanEqual, '<=': lessThanEqual, and: andOperator, or: orOperator, 'in': inOperator, '=': setVar, '[': arrayIndex }; this.ternaryOps = { '?': condition }; this.functions = { random: random, fac: factorial, min: min, max: max, hypot: Math.hypot || hypot, pyt: Math.hypot || hypot, // backward compat pow: Math.pow, atan2: Math.atan2, 'if': condition, gamma: gamma, roundTo: roundTo, map: arrayMap, fold: arrayFold, filter: arrayFilter, indexOf: stringOrArrayIndexOf, join: arrayJoin }; this.consts = { E: Math.E, PI: Math.PI, 'true': true, 'false': false }; } Parser.prototype.parse = function (expr) { var instr = []; var parserState = new ParserState( this, new TokenStream(this, expr), { allowMemberAccess: this.options.allowMemberAccess } ); parserState.parseExpression(instr); parserState.expect(TEOF, 'EOF'); return new Expression(instr, this); }; Parser.prototype.evaluate = function (expr, variables) { return this.parse(expr).evaluate(variables); }; var sharedParser = new Parser(); Parser.parse = function (expr) { return sharedParser.parse(expr); }; Parser.evaluate = function (expr, variables) { return sharedParser.parse(expr).evaluate(variables); }; var optionNameMap = { '+': 'add', '-': 'subtract', '*': 'multiply', '/': 'divide', '%': 'r