UNPKG

evaluator.js

Version:
301 lines (240 loc) 8.3 kB
import { symbols, operators, constants, methods } from './config'; const isSymbol = (token: string): boolean => Object.keys(symbols).includes(token); const isOperator = (token: string): boolean => Object.keys(operators).includes(token); const isMethod = (token: string): boolean => Object.keys(methods).includes(token); const isConstant = (token: string): boolean => Object.keys(constants).includes(token); const isNumber = (token: string): boolean => /(\d+\.\d*)|(\d*\.\d+)|(\d+)/.test(token); const isOpenParenthesis = (token: string): boolean => /\(/.test(token); const isCloseParenthesis = (token: string): boolean => /\)/.test(token); const isComma = (token: string): boolean => /,/.test(token); const isWhitespace = (token: string): boolean => /\s/.test(token); const round = (number: number, precision: number): number => { const modifier = 10 ** precision; return !modifier ? Math.round(number) : Math.round(number * modifier) / modifier; }; function topOperatorHasPrecedence(operatorStack: string[], currentOperatorName: string): boolean { if (!operatorStack.length) return false; const topToken = operatorStack[operatorStack.length - 1]; if (!isOperator(topToken)) return false; const topOperator = operators[topToken]; const 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: string, previousToken: string): string { 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: string): string[] { if (!expression.length) { throw Error('No input'); } const pattern = /(\d+\.\d*)|(\d*\.\d+)|(\d+)|([a-zA-Z0-9_]+)|(.)/g; const infixExpression = (expression.match(pattern) || []) .filter((token) => !isWhitespace(token)) .map((token) => 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: string[]): string[] { if (!infixExpression.length) { throw Error('No valid tokens'); } const operatorStack = []; const arityStack = []; const postfixExpression = []; let methodIsNewlyDeclared = false; infixExpression.forEach((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)) { const operatorName = determineOperator(token, infixExpression[index - 1]); const 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])) { const method = operatorStack[operatorStack.length - 1]; const 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) { const 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: string[]): number { if (!postfixExpression.length) { throw Error('No operations'); } const evaluationStack = []; postfixExpression.forEach((token) => { if (isMethod(String(token).split(':')[0])) { const [methodName, argumentCount] = token.split(':'); const method = methods[methodName]; const isVariadic = method.length === 0; const requiredArguments = isVariadic ? 1 : method.length; if (evaluationStack.length < requiredArguments) { throw Error(`Insufficient arguments for method: ${token}`); } const result = method(...evaluationStack.splice(isVariadic ? -argumentCount : -method.length)); evaluationStack.push(result); return; } if (isConstant(token)) { evaluationStack.push(constants[token]); return; } if (isNumber(token)) { evaluationStack.push(token); return; } const 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'); } const result = operator.method(...evaluationStack.splice(-operator.method.length)); evaluationStack.push(result); }); if (evaluationStack.length > 1) { throw Error('Insufficient operators'); } const reduction = evaluationStack[0]; const 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. */ export default function (expression: string): number { try { const infixExpression = parse(expression); const postfixExpression = convert(infixExpression); const result = resolve(postfixExpression); return result; } catch (error) { throw error; } }