This is an even older thread now, but knowing it still gets attention from search engines (I was brought here this way), here's my take on the answer, as I recently needed something similar, but in pure JavaScript, without any additional dependencies. My requirement was it to be able to perform simple formula evaluation (e.g., we have a record in the database representing a rectangle with dimensions and we want to calculate its area).
/**
*
* @param {string} formula
* @param {function(placeholder: string): number} [placeholderResolver]
* @return number
*/
const evaluateFormula = (formula, placeholderResolver) => {
const validOperandRegex = /\{[^}]+}|[a-zA-Z][a-zA-Z0-9._]*|\d+\.?\d*/; // Enclosed in {}, starting with a letter, or a number
const multiplicationDivisionRegex = new RegExp('(' + validOperandRegex.source + ')' + String.raw`\s*(\*|\/)\s*` + '(' + validOperandRegex.source + ')');
const additionSubtractionRegex = new RegExp('(' + validOperandRegex.source + ')' + String.raw`\s*(\+|\-)\s*` + '(' + validOperandRegex.source + ')');
/**
*
* @param {string} formula
* @param {function(placeholder: string): number} [placeholderResolver]
* @return number
*/
const evaluateFormulaInner = (formula, placeholderResolver) => {
if (!placeholderResolver) {
placeholderResolver = x => Number(x);
}
// Solve parenthesised expressions first
const originalFormula = formula.trim();
formula = formula.replace(/\(([^)]+)\)/g,
(substring, subFormula) => {
console.log(`Solving parens: ${subFormula}`);
return evaluateFormulaInner(subFormula, placeholderResolver).toString();
});
if (formula !== originalFormula) {
// There were parenthesis in the formula
console.log(`Parens stripped: ${formula}`);
return evaluateFormulaInner(formula, placeholderResolver);
}
// Solve multiplications and divisions
// Note - only the first encountered expression is evaluated to avoid breaking the sequence of consecutive operations
// e.g., 2 * 2 / 4 * 4 equals 4 / 4 * 4 (=4), not 4 / 16 (=1/4)
formula = formula.replace(multiplicationDivisionRegex, (substring, operandA, operator, operandB) => {
return evaluateSimpleEquation(operandA, operator, operandB, placeholderResolver);
});
if (formula !== originalFormula) {
// There were divisions or multiplications
console.log(`Replaced divisions or multiplications: ${formula}`);
return evaluateFormulaInner(formula, placeholderResolver);
}
// Solve additions and subtractions
// Note - only the first encountered expression is evaluated to avoid breaking the sequence of consecutive operations
// e.g., 2 + 2 - 4 + 4 equals 4 - 4 + 4 (=4), not 4 - 8 (=-4)
formula = formula.replace(additionSubtractionRegex, (substring, operandA, operator, operandB) => {
return evaluateSimpleEquation(operandA, operator, operandB, placeholderResolver);
});
if (formula !== originalFormula) {
// There were additions or subtractions
console.log(`Replaced additions or subtractions: ${formula}`);
return evaluateFormulaInner(formula, placeholderResolver);
}
// Finally, we can convert the answer to a number
return evaluateOperandValue(formula, placeholderResolver);
};
/**
*
* @param {string} operand
* @param {function(placeholder: string): number} placeholderResolver
*/
const evaluateOperandValue = (operand, placeholderResolver) => {
operand = operand.trim();
if (operand.match(/^\{[^}]+}$/)) {
return placeholderResolver(operand.slice(1, -1));
}
if (operand.match(/^[a-z][a-z0-9._]*$/)) {
return placeholderResolver(operand);
}
const result = Number(operand);
if (isNaN(result)) {
throw new Error('Invalid operand: ' + operand);
}
return result;
}
/**
*
* @param {string} operandA
* @param {'*'|'/'|'+'|'-'} operator
* @param {string} operandB
* @param {function(placeholder: string): number} placeholderResolver
*/
const evaluateSimpleEquation = (operandA, operator, operandB, placeholderResolver) => {
const operandANumber = evaluateOperandValue(operandA, placeholderResolver);
const operandBNumber = evaluateOperandValue(operandB, placeholderResolver);
switch (operator) {
case "*":
return operandANumber * operandBNumber;
case "+":
return operandANumber + operandBNumber;
case "-":
return operandANumber - operandBNumber;
case "/":
return operandANumber / operandBNumber;
}
throw new Error('Invalid operator: ' + operator);
};
return evaluateFormulaInner(formula, placeholderResolver)
};
The function then allows to solve simple problems like the OP's, but also problems requiring some flexibility, like mine:
const rectangles = [
{width: 10, height: 8},
{width: 4, height: 4},
{width: 3, height: 8},
];
rectangles.forEach(rectangle => {
console.log(`Rectangle area (${rectangle.width} x ${rectangle.height}) is ${evaluateFormula('width * height', (dimension) => rectangle[dimension])}`);
});
Result:
Rectangle area (10 x 8) is 80
Rectangle area (4 x 4) is 16
Rectangle area (3 x 8) is 24
variable symbols
? likesym
in matlab? you pretty much depend on that. – Hilleval
in a loop, you can run it throughnew Function('x', 'return 1 + x;');
- this will only require the string to be evaluated once, returning a function that you can then pass values forx
to. – Weigela