71

Is there a way to calculate a formula stored in a string in JavaScript without using eval()?

Normally I would do something like

var apa = "12/5*9+9.4*2";
console.log(eval(apa));

So, does anyone know about alternatives to eval()?

3
  • 9
    There is nothing wrong with eval as long as you can be sure about what the string you are evaluating contains. Commented Jun 25, 2011 at 17:05
  • 1
    I agree with Felix. Any other way wouldn't be as efficient. However, I have provided a pure JS solution (w/o eval). Commented Jun 26, 2011 at 8:00
  • 8
    In case you wondered, using eval in a web app is not a security risk. If someone wants to inject code into your web app, they can just open it up in the chrome debugger and modify the code! The only time you need to worry about security is if you are using eval() on the server, such as node.js Commented Mar 9, 2019 at 15:52

12 Answers 12

143

You could use the Function() constructor, which is similar to eval (never run it on untrusted input):

function evil(fn) {
  return new Function('return ' + fn)();
}

console.log(evil('12/5*9+9.4*2')); // => 40.4

Sign up to request clarification or add additional context in comments.

9 Comments

You, sir, are awesome. I hadn't known about this until your post. +1 upvote from me, and I've already used it in a project. Goodbye eval()!
But keep in mind that using the Function constructor in this way is similar to eval() in that the risks may be comparable. In this case, because the string is assumed to be safe and trusted, we would expect use of either eval() or the Function constructor to be reasonable.
@davisec52 That's why I called it eviiil ;)
Chrome's Content Security Policy will still throw EvalError
Creating constructor doesn't make sense here and it takes unnecessary resources. Result is the same as without the 'new'. It would differ in rare case, if this function evil() itself declared some variables which would want to be avoided. But then, this approach is faster than eval() for the reasons of variable lookup. Here is nice explanation and more examples.
|
62

There's nothing wrong with eval, especially for cases like this. You can sanitize the string with a regex first to be safe:

// strip anything other than digits, (), -+/* and .
var str = "12/5*9+9.4*2".replace(/[^-()\d/*+.]/g, '');
console.log(eval(str));

Comments

31

Eval was built for conditions like this.

If you wanted another method, you'd have to use a pure Javascript implementation of the exact thing eval is going to do.

  • The hard part is not the parsing of numbers and operators
  • The hard part is applying order of operation and recursive control

Here's a quick basic example I came up with (updated (2011-06-26): cleaner w/ input boxes).
http://jsfiddle.net/vol7ron/6cdfA/

Note:

  • it only handles the basic operators
  • it does not check the validity of the numbers (example: divide by zero)
  • it has not implemented parenthetical operation
  • for all these reasons and more, eval would be a better choice

Edit (2017-05-26) to use SO Snippet:

function calculate(input) {

  var f = {
    add: '+',
    sub: '-',
    div: '/',
    mlt: '*',
    mod: '%',
    exp: '^'
  };

  // Create array for Order of Operation and precedence
  f.ooo = [
    [
      [f.mlt],
      [f.div],
      [f.mod],
      [f.exp]
    ],
    [
      [f.add],
      [f.sub]
    ]
  ];

  input = input.replace(/[^0-9%^*\/()\-+.]/g, ''); // clean up unnecessary characters

  var output;
  for (var i = 0, n = f.ooo.length; i < n; i++) {

    // Regular Expression to look for operators between floating numbers or integers
    var re = new RegExp('(\\d+\\.?\\d*)([\\' + f.ooo[i].join('\\') + '])(\\d+\\.?\\d*)');
    re.lastIndex = 0; // take precautions and reset re starting pos

    // Loop while there is still calculation for level of precedence
    while (re.test(input)) {
      output = _calculate(RegExp.$1, RegExp.$2, RegExp.$3);
      if (isNaN(output) || !isFinite(output)) 
        return output; // exit early if not a number
      input = input.replace(re, output);
    }
  }

  return output;

  function _calculate(a, op, b) {
    a = a * 1;
    b = b * 1;
    switch (op) {
      case f.add:
        return a + b;
        break;
      case f.sub:
        return a - b;
        break;
      case f.div:
        return a / b;
        break;
      case f.mlt:
        return a * b;
        break;
      case f.mod:
        return a % b;
        break;
      case f.exp:
        return Math.pow(a, b);
        break;
      default:
        null;
    }
  }
}
label {
  display: inline-block;
  width: 4em;
}
<div>
  <label for="input">Equation: </label>
  <input type="text" id="input" value="12/5*9+9.4*2-1" />
  <input type="button" 
         value="calculate" 
         onclick="getElementById('result').value = calculate(getElementById('input').value)" />
</div>

<div>
  <label for="result">Result: </label>
  <input type="text" id="result" />
</div>

1 Comment

(/[^0-9%^*\/()\-+.]/g, '');: ^ does not need to be escaped?
27

Here is an implementation of the Shunting-yard algorithm with additional support for unary prefix (e.g. -) and postfix (e.g. !) operators, and function (e.g. sqrt()) notations. More operators/functions can be easily defined with the Calculation.defineOperator method:

"use strict";
class Calculation {
    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);
        this.defineOperator("pi", Math.PI); // A constant
    }
    // 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;
    }
    calculate(expression) {
        let match;
        const values = [],
            operators = [this._symbols["("].prefix],
            exec = _ => {
                let op = operators.pop();
                values.push(op.f(...[].concat(...values.splice(-op.argCount))));
                return op.precedence;
            },
            error = msg => {
                let 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)
                        // longer symbols should be listed first
                        .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);
            let [token, bad] = match || [")", undefined];
            // Replace constant names (like PI) with corresponding value
            if (typeof this._symbols[token]?.func?.f === "number") token = this._symbols[token].func?.f;
            const 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!
    }
}
Calculation = new Calculation(); // Create a singleton

// I/O handling
function perform() {
    const expr = document.getElementById('expr').value,
        result = Calculation.calculate(expr);
    document.getElementById('out').textContent = isNaN(result) ? result : '=' + result;
}
document.getElementById('expr').addEventListener('input', perform);
perform();

// Tests
const tests = [
    { expr: '1+2', expected: 3 },
    { expr: '1+2*3', expected: 7 },
    { expr: '1+2*3^2', expected: 19 },
    { expr: '1+2*2^3^2', expected: 1025 },
    { expr: '-3!', expected: -6 },
    { expr: '12---11+1-3', expected: -1 },
    { expr: 'min(2,1,3)', expected: 1 },
    { expr: '(2,1,3)', expected: 3 },
    { expr: '4-min(sqrt(2+2*7),9,5)', expected: 0 },
    { expr: '2,3,10', expected: 10 },
    { expr: 'pi*2', expected: Math.PI*2 },
]

for (let {expr, expected} of tests) {
    let result = Calculation.calculate(expr);
    console.assert(result === expected, `${expr} should be ${expected}, but gives ${result}`);
}
#expr { width: 100%; font-family: monospace }
Expression: <input id="expr" value="min(-1,0)+((sqrt(16)+(-4+7)!*---4)/2)^2^3"><p>
<pre id="out"></pre>

2 Comments

Really amazing work! How would you please implement * as the default operation? Ex: (1)1 throws error while it should be treated as (1)*1.
Thank you. If you cannot make it work with a default operator, I suggest you ask a new question, pointing out where exactly you get stuck.
26

This is exactly the place where you should be using eval(), or you will have to loop through the string and generate the numbers. You will have to use the Number.isNaN() method to do it.

2 Comments

Only if the string comes from a trusted source.
It's december of 2021, and the java world has been rattled by a log4j vulnerability. Exactly because of this.
10

If you don't want to use eval you will have to use an existing expression evaluator library.

http://silentmatt.com/javascript-expression-evaluator/

http://www.codeproject.com/KB/scripting/jsexpressioneval.aspx

You can also roll one of your own :)

1 Comment

Probably the best answer. Every other answer either uses ify regular expressions or use eval() with one step removed.
4

I spent a couple of hours to implement all the arithmetical rules without using eval() and finally I published a package on npm string-math. Everything is in the description. Enjoy

Comments

4

This solution also clips whitespaces and checks for duplicating operators

e.g. ' 1+ 2 *2' // 5 but ' 1 + +2* 2 ' // Error

function calcMe(str) {
  const noWsStr = str.replace(/\s/g, '');
  const operators = noWsStr.replace(/[\d.,]/g, '').split('');
  const operands = noWsStr.replace(/[+/%*-]/g, ' ')
                          .replace(/\,/g, '.')
                          .split(' ')
                          .map(parseFloat)
                          .filter(it => it);

  if (operators.length >= operands.length){
    throw new Error('Operators qty must be lesser than operands qty')
  };

  while (operators.includes('*')) {
    let opIndex = operators.indexOf('*');
    operands.splice(opIndex, 2, operands[opIndex] * operands[opIndex + 1]);
    operators.splice(opIndex, 1);
  };
  while (operators.includes('/')) {
    let opIndex = operators.indexOf('/');
    operands.splice(opIndex, 2, operands[opIndex] / operands[opIndex + 1]);
    operators.splice(opIndex, 1);
  };
  while (operators.includes('%')) {
    let opIndex = operators.indexOf('%');
    operands.splice(opIndex, 2, operands[opIndex] % operands[opIndex + 1]);
    operators.splice(opIndex, 1);
  };

  let result = operands[0];
  for (let i = 0; i < operators.length; i++) {
    operators[i] === '+' ? (result += operands[i + 1]) : (result -= operands[i + 1])
  }
  return result
}

This shows to be more performant than @vol7ron's solution. Check this JSBenchmark

1 Comment

Braces are not supported, am I right? Anyway, truly bold attempt!
3

If you're looking for a syntactical equivalent to eval, you could use new Function. There are slight differences regarding scoping, but they mostly behave the same, including exposure to much of the same security risks:

let str = "12/5*9+9.4*2"

let res1 = eval(str)
console.log('res1:', res1)

let res2 = (new Function('return '+str)())
console.log('res2:', res2)

Comments

3

You can't, at most you could do something retort like parsing the numbers and then separating the operations with a switch, and making them. Other than that, I'd use eval in this case.

That would be something like (a real implementation will be somewhat more complex, especially if you consider the use of parenthesis, but you get the idea)

function operate(text) {
  var values = text.split("+");

  return parseInt(values[0]) + parseInt(values[1]);
}

console.log(operate("9+2"));

Still, I think the best choice you can make is to use eval, given that you're able to trust the source of the string.

Comments

1

There is also an open source implementation on GitHub, evaluator.js, and an NPM package.

From the README: Evaluator.js is a small, zero-dependency module for evaluating mathematical expressions.

All major operations, constants, and methods are supported. Additionally, Evaluator.js intelligently reports invalid syntax, such as a misused operator, missing operand, or mismatched parentheses.

Evaluator.js is used by a desktop calculator application of the same name. See a live demo on the website.

Comments

1

Note : There is no library used in this solution purely hard coded

My solution takes into account of brackets also like 8+6(7(-1)) or 8+6(7(-1))

You can do these operations ^, *, /, +, -

To calculate a string use calculate(tokenize(pieval("8+6(7(-1))").join("")))

function tokenize(s) {
    // --- Parse a calculation string into an array of numbers and operators
    const r = [];
    let token = '';
    for (const character of s) {
        if ('^*/+-'.indexOf(character) > -1) {
            if (token === '' && character === '-') {
                token = '-';
            } else {
                r.push(parseFloat(token), character);
                token = '';
            }
        } else {
            token += character;
        }
    }
    if (token !== '') {
        r.push(parseFloat(token));
    }
    return r;
}

function calculate(tokens) {
    // --- Perform a calculation expressed as an array of operators and numbers
    const operatorPrecedence = [{'^': (a, b) => Math.pow(a, b)},
               {'*': (a, b) => a * b, '/': (a, b) => a / b},
               {'+': (a, b) => a + b, '-': (a, b) => a - b}];
    let operator;
    for (const operators of operatorPrecedence) {
        const newTokens = [];
        for (const token of tokens) {
            if (token in operators) {
                operator = operators[token];
            } else if (operator) {
                newTokens[newTokens.length - 1] = 
                    operator(newTokens[newTokens.length - 1], token);
                operator = null;
            } else {
                newTokens.push(token);
            }
        }
        tokens = newTokens;
    }
    if (tokens.length > 1) {
        console.log('Error: unable to resolve calculation');
        return tokens;
    } else {
        return tokens[0];
    }
}

function pieval(input) {
  let openParenCount = 0;
  let myOpenParenIndex = 0;
  let myEndParenIndex = 0;
  const result = [];

  for (let i = 0; i < input.length; i++) {
    if (input[i] === "(") {
      if (openParenCount === 0) {
        myOpenParenIndex = i;

        // checking if anything exists before this set of parentheses
        if (i !== myEndParenIndex) {
            if(!isNaN(input[i-1])){
                result.push(input.substring(myEndParenIndex, i) + "*");
            }else{
                result.push(input.substring(myEndParenIndex, i));
            }
        }
      }
      openParenCount++;
    }

    if (input[i] === ")") {
      openParenCount--;
      if (openParenCount === 0) {
        myEndParenIndex = i + 1;

        // recurse the contents of the parentheses to search for nested ones
        result.push(pieval(input.substring(myOpenParenIndex + 1, i)));
      }
    }
  }

  // capture anything after the last parentheses
  if (input.length > myEndParenIndex) {
    result.push(input.substring(myEndParenIndex, input.length));
  }

  //console.log(cal(result))
  let response = cal(result);
  return result;
}

function cal(arr) {
  let calstr = "";
  for (let i = 0; i < arr.length; i++) {
    if (typeof arr[i] != "string") {
      if (cal(arr[i]) < 0) {
        arr[i] = `${cal(arr[i])}`;
      } else {
        arr[i] = `${cal(arr[i])}`;
      }
    }
    if (typeof arr[i] === "string") {
      calstr += arr[i];
    }
    if (i == arr.length - 1) {
      //console.log("cal" ,calstr,calculate(tokenize(calstr)) );
      return calculate(tokenize(calstr));
    }
  }
}
console.log(calculate(tokenize(pieval("8+6(7(-1))").join("")))); // ["1+",["2-",["3+4"]]]
console.log(calculate(tokenize(pieval("1+(1+(2(4/4))+4)").join("")))); // ["1+",["2-",["3+4"]]]

1 Comment

This answer copies and extends my answer to a similar question

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.