evaluator.js
Version:
Evaluates mathematical expressions
449 lines (380 loc) • 12.7 kB
JavaScript
var symbols = {
'^': {
infix: '_POW'
},
'*': {
infix: '_MUL'
},
'/': {
infix: '_DIV'
},
'%': {
infix: '_MOD'
},
'+': {
infix: '_ADD',
prefix: '_POS'
},
'-': {
infix: '_SUB',
prefix: '_NEG'
}
};
var factorial = function (x) { return x >= 0 ? x < 2 ? 1 : x * factorial(x - 1) : NaN; };
var operators = {
'_POW': {
name: 'Power',
precedence: 4,
associativity: 'right',
method: function (x, y) { return Math.pow( x, y ); }
},
'_POS': {
name: 'Positive',
precedence: 3,
associativity: 'right',
method: function (x) { return x; }
},
'_NEG': {
name: 'Negative',
precedence: 3,
associativity: 'right',
method: function (x) { return -x; }
},
'_MUL': {
name: 'Multiply',
precedence: 2,
associativity: 'left',
method: function (x, y) { return x * y; }
},
'_DIV': {
name: 'Divide',
precedence: 2,
associativity: 'left',
method: function (x, y) { return x / y; }
},
'_MOD': {
name: 'Modulo',
precedence: 2,
associativity: 'left',
method: function (x, y) { return x % y; }
},
'_ADD': {
name: 'Add',
precedence: 1,
associativity: 'left',
method: function (x, y) { return x + y; }
},
'_SUB': {
name: 'Subtract',
precedence: 1,
associativity: 'left',
method: function (x, y) { return x - y; }
}
};
var constants = {
'E': Math.E,
'LN2': Math.LN2,
'LN10': Math.LN10,
'LOG2E': Math.LOG2E,
'LOG10E': Math.LOG10E,
'PHI': (1 + Math.sqrt(5)) / 2,
'PI': Math.PI,
'SQRT1_2': Math.SQRT1_2,
'SQRT2': Math.SQRT2,
'TAU': 2 * Math.PI
};
var methods = {
'ABS': function (x) { return Math.abs(x); },
'ACOS': function (x) { return Math.acos(x); },
'ACOSH': function (x) { return Math.acosh(x); },
'ADD': function (x, y) { return x + y; },
'ASIN': function (x) { return Math.asin(x); },
'ASINH': function (x) { return Math.asinh(x); },
'ATAN': function (x) { return Math.atan(x); },
'ATANH': function (x) { return Math.atanh(x); },
'ATAN2': function (y, x) { return Math.atan2(y, x); },
'CBRT': function (x) { return Math.cbrt(x); },
'CEIL': function (x) { return Math.ceil(x); },
'COS': function (x) { return Math.cos(x); },
'COSH': function (x) { return Math.cosh(x); },
'DIVIDE': function (x, y) { return x / y; },
'EXP': function (x) { return Math.exp(x); },
'EXPM1': function (x) { return Math.expm1(x); },
'FACTORIAL': factorial,
'FLOOR': function (x) { return Math.floor(x); },
'HYPOT': function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return Math.hypot.apply(Math, args);
},
'LOG': function (x) { return Math.log(x); },
'LOG1P': function (x) { return Math.log1p(x); },
'LOG10': function (x) { return Math.log10(x); },
'LOG2': function (x) { return Math.log2(x); },
'MAX': function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return Math.max.apply(Math, args);
},
'MEAN': function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return [].concat( args ).reduce(function (sum, x) {
return sum + x;
}, 0) / [].concat( args ).length;
},
'MIN': function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return Math.min.apply(Math, args);
},
'MOD': function (x, y) { return x % y; },
'MULTIPLY': function (x, y) { return x * y; },
'POW': function (x, y) { return Math.pow( x, y ); },
'SIN': function (x) { return Math.sin(x); },
'SINH': function (x) { return Math.sinh(x); },
'SQRT': function (x) { return Math.sqrt(x); },
'SUBTRACT': function (x, y) { return x - y; },
'SUM': function () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return [].concat( args ).reduce(function (sum, x) {
sum += x;
return sum;
}, 0);
},
'TAN': function (x) { return Math.tan(x); },
'TANH': function (x) { return Math.tanh(x); }
};
var isSymbol = function (token) { return Object.keys(symbols).includes(token); };
var isOperator = function (token) { return Object.keys(operators).includes(token); };
var isMethod = function (token) { return Object.keys(methods).includes(token); };
var isConstant = function (token) { return Object.keys(constants).includes(token); };
var isNumber = function (token) { return /(\d+\.\d*)|(\d*\.\d+)|(\d+)/.test(token); };
var isOpenParenthesis = function (token) { return /\(/.test(token); };
var isCloseParenthesis = function (token) { return /\)/.test(token); };
var isComma = function (token) { return /,/.test(token); };
var isWhitespace = function (token) { return /\s/.test(token); };
var round = function (number, precision) {
var modifier = Math.pow( 10, precision );
return !modifier ? Math.round(number) : Math.round(number * modifier) / modifier;
};
function topOperatorHasPrecedence(operatorStack, currentOperatorName) {
if (!operatorStack.length) { return false; }
var topToken = operatorStack[operatorStack.length - 1];
if (!isOperator(topToken)) { return false; }
var topOperator = operators[topToken];
var currentOperator = operators[currentOperatorName];
if (currentOperator.method.length === 1 && topOperator.method.length > 1) { return false; }
if (topOperator.precedence > currentOperator.precedence) { return true; }
return topOperator.precedence === currentOperator.precedence && topOperator.associativity === 'left';
}
function determineOperator(token, previousToken) {
if (previousToken === undefined || isOpenParenthesis(previousToken) || isSymbol(previousToken) || isComma(previousToken)) {
return symbols[token].prefix;
}
if (isCloseParenthesis(previousToken) || isNumber(previousToken) || isConstant(previousToken)) {
return symbols[token].infix;
}
return undefined;
}
/**
* Takes a string and parses out the array of tokens in infix notation.
*
* @param {string} expression The string.
*
* @throws {Error} No input.
*
* @returns {string[]} The array of tokens in infix notation.
*/
function parse(expression) {
if (!expression.length) {
throw Error('No input');
}
var pattern = /(\d+\.\d*)|(\d*\.\d+)|(\d+)|([a-zA-Z0-9_]+)|(.)/g;
var infixExpression = (expression.match(pattern) || []).filter(function (token) { return !isWhitespace(token); }).map(function (token) { return token.toUpperCase(); });
return infixExpression;
}
/**
* Takes an array of tokens in infix notation and converts it to postfix notation.
*
* @param {string[]} infixExpression The array of tokens in infix notation.
*
* @throws {Error} No valid tokens.
* @throws {Error} Misused operator: <token>.
* @throws {Error} Mismatched parentheses.
* @throws {Error} Invalid token: <token>.
* @throws {Error} Insufficient arguments for method: <token>.
*
* @returns {string[]} The array of tokens in postfix notation.
*/
function convert(infixExpression) {
if (!infixExpression.length) {
throw Error('No valid tokens');
}
var operatorStack = [];
var arityStack = [];
var postfixExpression = [];
var methodIsNewlyDeclared = false;
infixExpression.forEach(function (token, index) {
if (methodIsNewlyDeclared && !isOpenParenthesis(token)) {
throw Error(("Misused method: " + (operatorStack[operatorStack.length - 1])));
}
methodIsNewlyDeclared = false;
if (isMethod(token)) {
methodIsNewlyDeclared = true;
operatorStack.push(token);
arityStack.push(1);
return;
}
if (isConstant(token)) {
postfixExpression.push(token);
return;
}
if (isNumber(token)) {
postfixExpression.push(parseFloat(token));
return;
}
if (isSymbol(token)) {
var operatorName = determineOperator(token, infixExpression[index - 1]);
var operator = operators[operatorName];
if (operator === undefined) {
throw Error(("Misused operator: " + token));
}
while (topOperatorHasPrecedence(operatorStack, operatorName)) {
postfixExpression.push(operatorStack.pop());
}
operatorStack.push(operatorName);
return;
}
if (isOpenParenthesis(token)) {
operatorStack.push(token);
return;
}
if (isComma(token)) {
arityStack[arityStack.length - 1] += 1;
while (!isOpenParenthesis(operatorStack[operatorStack.length - 1])) {
if (!operatorStack.length) {
throw Error('Invalid token: ,');
}
postfixExpression.push(operatorStack.pop());
}
return;
}
if (isCloseParenthesis(token)) {
while (!isOpenParenthesis(operatorStack[operatorStack.length - 1])) {
if (!operatorStack.length) {
throw Error('Mismatched parentheses');
}
postfixExpression.push(operatorStack.pop());
}
operatorStack.pop();
if (isMethod(operatorStack[operatorStack.length - 1])) {
var method = operatorStack[operatorStack.length - 1];
var argumentCount = arityStack.pop();
if (argumentCount < methods[method].length) {
throw Error(("Insufficient arguments for method: " + method));
}
postfixExpression.push(((operatorStack.pop()) + ":" + argumentCount));
}
return;
}
throw Error(("Invalid token: " + token));
});
while (operatorStack.length) {
var operator = operatorStack[operatorStack.length - 1];
if (isOpenParenthesis(operator) || isCloseParenthesis(operator)) {
throw Error('Mismatched parentheses');
}
postfixExpression.push(operatorStack.pop());
}
return postfixExpression;
}
/**
* Takes an array of tokens in postfix notation and resolves the result.
*
* @param {string[]} postfixExpression The array of tokens in postfix notation.
*
* @throws {Error} No operations.
* @throws {Error} Insufficient arguments for method: <token>.
* @throws {Error} Insufficient operands for operator: <token>.
* @throws {Error} Division by zero.
* @throws {Error} Insufficient operators.
*
* @returns {number} The result.
*/
function resolve(postfixExpression) {
if (!postfixExpression.length) {
throw Error('No operations');
}
var evaluationStack = [];
postfixExpression.forEach(function (token) {
if (isMethod(String(token).split(':')[0])) {
var ref = token.split(':');
var methodName = ref[0];
var argumentCount = ref[1];
var method = methods[methodName];
var isVariadic = method.length === 0;
var requiredArguments = isVariadic ? 1 : method.length;
if (evaluationStack.length < requiredArguments) {
throw Error(("Insufficient arguments for method: " + token));
}
var result$1 = method.apply(void 0, evaluationStack.splice(isVariadic ? -argumentCount : -method.length));
evaluationStack.push(result$1);
return;
}
if (isConstant(token)) {
evaluationStack.push(constants[token]);
return;
}
if (isNumber(token)) {
evaluationStack.push(token);
return;
}
var operator = operators[token];
if (evaluationStack.length < operator.method.length) {
throw Error(("Insufficient operands for operator: " + (operator.name)));
}
if (token === '_DIV' && evaluationStack[evaluationStack.length - 1] === 0) {
throw Error('Division by zero');
}
var result = operator.method.apply(operator, evaluationStack.splice(-operator.method.length));
evaluationStack.push(result);
});
if (evaluationStack.length > 1) {
throw Error('Insufficient operators');
}
var reduction = evaluationStack[0];
var result = round(reduction, 8);
return result;
}
/**
* Takes a string and evaluates the result.
*
* @param {string} expression The string.
*
* @throws {Error} No input.
* @throws {Error} No valid tokens.
* @throws {Error} Misused operator: <token>.
* @throws {Error} Mismatched parentheses.
* @throws {Error} Invalid token: <token>.
* @throws {Error} No operations.
* @throws {Error} Insufficient arguments for method: <token>.
* @throws {Error} Insufficient operands for operator: <token>.
* @throws {Error} Division by zero.
* @throws {Error} Insufficient operators.
*
* @returns {number} The result.
*/
function index (expression) {
try {
var infixExpression = parse(expression);
var postfixExpression = convert(infixExpression);
var result = resolve(postfixExpression);
return result;
} catch (error) {
throw error;
}
}
module.exports = index;
//# sourceMappingURL=evaluator.js.map