UNPKG

evaluator.js

Version:
449 lines (380 loc) 12.7 kB
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; } } export default index; //# sourceMappingURL=evaluator.m.js.map