evaluator.js
Version:
Evaluates mathematical expressions
301 lines (240 loc) • 8.3 kB
text/typescript
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;
}
}