Calculate string value in javascript, not using eval

JavascriptEval

Javascript Problem Overview


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";
alert(eval(apa));

So, does anyone know about alternatives to eval?

Javascript Solutions


Solution 1 - Javascript

Mhh, you could use the Function-constructor:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function

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

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

Solution 2 - Javascript

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, '');
alert(eval(str));

Solution 3 - Javascript

This 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 isNaN method to do it.

Solution 4 - Javascript

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>

Solution 5 - Javascript

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);
    }
    // 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);
            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!
    }
}
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 }
]

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>

Solution 6 - Javascript

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 :)

Solution 7 - Javascript

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, specially 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]);
    }

    alert(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.

Solution 8 - Javascript

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

Solution 9 - Javascript

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)

Solution 10 - Javascript

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

Solution 11 - Javascript

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.

Solution 12 - Javascript

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"]]]

Solution 13 - Javascript

stringToMath(str);

I am using stringToMath(str) instead of eval(), it is working alright to calculate simple string values.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionEric HerlitzView Question on Stackoverflow
Solution 1 - JavascriptyckartView Answer on Stackoverflow
Solution 2 - JavascriptNoneView Answer on Stackoverflow
Solution 3 - JavascriptTroy SKView Answer on Stackoverflow
Solution 4 - Javascriptvol7ronView Answer on Stackoverflow
Solution 5 - JavascripttrincotView Answer on Stackoverflow
Solution 6 - Javascriptparapura rajkumarView Answer on Stackoverflow
Solution 7 - JavascriptbevacquaView Answer on Stackoverflow
Solution 8 - JavascriptPawełView Answer on Stackoverflow
Solution 9 - Javascriptvol7ronView Answer on Stackoverflow
Solution 10 - JavascriptnikkypizzaView Answer on Stackoverflow
Solution 11 - JavascriptErik VullingsView Answer on Stackoverflow
Solution 12 - JavascriptPIYUSH PUNIYAView Answer on Stackoverflow
Solution 13 - JavascriptEndeavorwebsView Answer on Stackoverflow