math/calculate.js

/* eslint-disable no-mixed-spaces-and-tabs */
/**
* A function to calculate a string without using `eval()`.
*
* @since v2.5.2-beta.2
* @category Math
*/

class Calculator {
	/**
  * Creates a calculator
	* @since v2.5.2-beta.2
  */
	constructor() {
		this._symbols = {};
		this.defineOperator('!', this.factorial, 'postfix', 6);
		this.defineOperator('^', Math.pow, 'infix', 5, true);
		this.defineOperator('*', this.multiplication, 'infix', 4);
		this.defineOperator('/', this.division, 'infix', 4);
		this.defineOperator('+', this.last, 'prefix', 3);
		this.defineOperator('-', this.negation, 'prefix', 3);
		this.defineOperator('+', this.addition, 'infix', 2);
		this.defineOperator('-', this.subtraction, 'infix', 2);
		this.defineOperator(',', Array.of, 'infix', 1);
		this.defineOperator('(', this.last, 'prefix');
		this.defineOperator(')', null, 'postfix');
		this.defineOperator('min', Math.min);
		this.defineOperator('sqrt', Math.sqrt);
	}
	// Method allowing to extend an instance with more operators and functions:
	defineOperator(symbol, f, notation = 'func', precedence = 0, rightToLeft = false) {
		// Store operators keyed by their symbol/name. Some symbols may represent
		// different usages: e.g. "-" can be unary or binary, so they are also
		// keyed by their notation (prefix, infix, postfix, func):
		if (notation === 'func') precedence = 0;
		this._symbols[symbol] = Object.assign({}, this._symbols[symbol], {
			[notation]: {
				symbol, f, notation, precedence, rightToLeft,
				argCount: 1 + (notation === 'infix'),
			},
			symbol,
			regSymbol: symbol.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
                + (/\w$/.test(symbol) ? '\\b' : ''), // add a break if it's a name
		});
	}
	last(...a) { return a[a.length - 1]; }
	negation(a) { return -a; }
	addition(a, b) { return a + b; }
	subtraction(a, b) { return a - b; }
	multiplication(a, b) { return a * b; }
	division(a, b) { return a / b; }
	factorial(a) {
		if (a % 1 || !(+a >= 0)) return NaN;
		if (a > 170) return Infinity;
		let b = 1;
		while (a > 1) b *= a--;
		return b;
	}
	/**
	* Calculates an expression.
	* @example
	* const calculator = new Calculator();
	* calculator.calculate("min(-1,0)+((sqrt(16)+(-4+7)!*---4)/2)^2^3")
	*/
	calculate(expression) {
		let match;
		const values = [],
			operators = [this._symbols['('].prefix],
			exec = () => {
				const op = operators.pop();
				values.push(op.f(...[].concat(...values.splice(-op.argCount))));
				return op.precedence;
			},
			error = (msg) => {
				const notation = match ? match.index : expression.length;
				return `${msg} at ${notation}:\n${expression}\n${' '.repeat(notation)}^`;
			},
			pattern = new RegExp(
				// Pattern for numbers
				'\\d+(?:\\.\\d+)?|'
                // ...and patterns for individual operators/function names
                + Object.values(this._symbols).sort((a, b) => b.symbol.length - a.symbol.length).map(val => val.regSymbol).join('|')
                + '|(\\S)', 'g',
			);
		let afterValue = false;
		pattern.lastIndex = 0; // Reset regular expression object
		do {
			match = pattern.exec(expression);
			const [token, bad] = match || [')', undefined],
				notNumber = this._symbols[token],
				notNewValue = notNumber && !notNumber.prefix && !notNumber.func,
				notAfterValue = !notNumber || !notNumber.postfix && !notNumber.infix;
			// Check for syntax errors:
			if (bad || (afterValue ? notAfterValue : notNewValue)) return error('Syntax error');
			if (afterValue) {
				// We either have an infix or postfix operator (they should be mutually exclusive)
				const curr = notNumber.postfix || notNumber.infix;
				do {
					const prev = operators[operators.length - 1];
					if (((curr.precedence - prev.precedence) || prev.rightToLeft) > 0) break;
					// Apply previous operator, since it has precedence over current one
				} while (exec()); // Exit loop after executing an opening parenthesis or function
				afterValue = curr.notation === 'postfix';
				if (curr.symbol !== ')') {
					operators.push(curr);
					// Postfix always has precedence over any operator that follows after it
					if (afterValue) exec();
				}
			}
			else if (notNumber) { // prefix operator or function
				operators.push(notNumber.prefix || notNumber.func);
				if (notNumber.func) { // Require an opening parenthesis
					match = pattern.exec(expression);
					if (!match || match[0] !== '(') return error('Function needs parentheses');
				}
			}
			else { // number
				values.push(+token);
				afterValue = true;
			}
		} while (match && operators.length);
		return operators.length ? error('Missing closing parenthesis')
			: match ? error('Too many closing parentheses')
				: values.pop(); // All done!
	}
}

export { Calculator };