121

How do I parse and evaluate a mathematical expression in a string (e.g. '1+1') without invoking eval(string) to yield its numerical value?

With that example, I want the function to accept '1+1' and return 2.

nbrooks
  • 18,126
  • 5
  • 54
  • 66
wheresrhys
  • 22,558
  • 19
  • 94
  • 162
  • 10
    Very similar but it’s probably not what you’re asking for: `(Function("return 1+1;"))()`. – Gumbo Feb 16 '10 at 20:20

27 Answers27

98

You can use the JavaScript Expression Evaluator library, which allows you to do stuff like:

Parser.evaluate("2 ^ x", { x: 3 });

Or mathjs, which allows stuff like:

math.eval('sin(45 deg) ^ 2');

I ended up choosing mathjs for one of my projects.

Rafael Vega
  • 4,575
  • 4
  • 32
  • 50
27

Simple and elegant with Function()

function parse(str) {
  return Function(`'use strict'; return (${str})`)()
}

document.write( "1+2+3", '=' , parse("1+2+3"), '<br>');
document.write( "1 + 2 * 3", '=' , parse("1 + 2 * 3"), '<br>');

Caution: Not to be used on Production

Aniket Kudale
  • 423
  • 5
  • 9
  • can u please explain how it works ? I'm new to this syntax – pageNotfoUnd Jul 27 '20 at 14:59
  • Function("return (1+2+3)")(); - its an anonymous function. We are just executing the argument (function body). Function("{ return (1+2+3) }")(); – Aniket Kudale Jul 27 '20 at 18:01
  • ok how string is parsed ? & what's that (${str})`) -----`() ` this bracket at last? – pageNotfoUnd Jul 27 '20 at 18:05
  • 7
    I don't see how this is any better than eval. Before you run this server-side, beware of `parse('process.exit()')`. – Basti Nov 11 '20 at 07:43
  • see also here https://stackoverflow.com/questions/2573548/given-a-string-describing-a-javascript-function-convert-it-to-a-javascript-func – Friedrich -- Слава Україні Jul 16 '21 at 11:01
  • 1
    OMG, I don't know how this is not the top answer, it's simple, fast, it can evaluate any valid JS expression (not only math) and it's not dependent on third party libraries – vakarami Apr 27 '22 at 15:23
  • 2
    Be careful, this could be a serious security risk depending of where the user input comes from. This interprets any JavaScript code like `parse('alert("hello")')`. – thibpat Sep 09 '22 at 13:25
  • that not bad, you can add a regex test to check only math string. see https://stackoverflow.com/questions/2276021/evaluating-a-string-as-a-mathematical-expression-in-javascript/74340598#74340598 – Mister Jojo Nov 06 '22 at 23:40
  • Regex or not, that sounds like a security nightmare to me - I don't think this answer should be upvoted that much. I don't envy the next programmer that will have to modify that regex to support new requirements and then test that this doesn't open up vulnerabilities. I think in this case it may be better to bite the bullet and take a dedicated library that is thoroughly tested and maintained by the OS community. – Philippe Hebert Jan 06 '23 at 16:23
  • @vakarami it's not the top answer because it's a massive security hole. **Do not** use this in a production system. – Rory McCrossan Jun 20 '23 at 20:48
  • @RoryMcCrossan it's a security whole if the input comes from a bad actor, but this is not related to this question/answer. user input validation should be done always regardless of the approach. – vakarami Jul 14 '23 at 14:49
  • @vakarami that's true, but on the client side there is no form of user input validation which is anywhere close to strong enough, or which cannot be circumvented in a trivial manner. – Rory McCrossan Jul 14 '23 at 14:54
  • @RoryMcCrossan, again never in the question it mentions that this is a requirement in client side or server! So this solution is still valid and it's awesome and simple and handy, however it can only be securely used when the input (`str`) is already trusted. – vakarami Jul 25 '23 at 06:34
  • I understand that, and you understand that. However the average user finding this site from Google most likely will not understand that, which is why it's important that any suggestions which have very major security implications are flagged as such. – Rory McCrossan Jul 25 '23 at 08:19
26

You can do + or - easily:

function addbits(s) {
  var total = 0,
      s = s.match(/[+\-]*(\.\d+|\d+(\.\d+)?)/g) || [];
      
  while (s.length) {
    total += parseFloat(s.shift());
  }
  return total;
}

var string = '1+23+4+5-30';
console.log(
  addbits(string)
)

More complicated math makes eval more attractive- and certainly simpler to write.

adiga
  • 34,372
  • 9
  • 61
  • 83
kennebec
  • 269
  • 2
  • 2
  • 2
    +1 - Probably a bit more general than what I went with, but it won't work for my situation as I may have something like 1+-2, and I want the regex to exclude invalid statements too (I think yours would allow something like "+3+4+") – wheresrhys Mar 06 '10 at 12:02
  • I've posted below an updated answer with a shorter regular expression and allowing for spaces between operators – Stefan Gabos May 31 '17 at 10:22
  • instead of using .shift you can use .pop since you wanna add all numbers. .pop has O(1) . shift has O(n) – abdullah ajibade Feb 07 '23 at 11:00
19

Somebody has to parse that string. If it's not the interpreter (via eval) then it'll need to be you, writing a parsing routine to extract numbers, operators, and anything else you want to support in a mathematical expression.

So, no, there isn't any (simple) way without eval. If you're concerned about security (because the input you're parsing isn't from a source you control), maybe you can check the input's format (via a whitelist regex filter) before passing it to eval?

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
bdukes
  • 152,002
  • 23
  • 148
  • 175
  • 1
    It's not security that bothers me ( I already have a regexp for the job), it's more the load on the browser as I have to process a lot of strings like this. Could a custom parser feasibly be faster than eval()? – wheresrhys Feb 16 '10 at 20:24
  • 12
    @wheresrhys: Why would you think your parser, written in JS, is going to be faster than the system provided one (optimized, probably written in C or C++)? – Mehrdad Afshari Feb 16 '10 at 20:41
  • 4
    eval is by far the fastest way to do this. However, a regexp is generally not sufficient to ensure security. – levik Feb 16 '10 at 20:44
  • 1
    @wheresrhys: Why do you have a lot of strings like this? Are they being generated by a program? If so, the simplest way is to calculate the result before they are converted to strings. Otherwise, it's write-your-own-parser time. – Phil H May 02 '12 at 12:58
16

An alternative to the excellent answer by @kennebec, using a shorter regular expression and allowing spaces between operators

function addbits(s) {
    var total = 0;
    s = s.replace(/\s/g, '').match(/[+\-]?([0-9\.\s]+)/g) || [];
    while(s.length) total += parseFloat(s.shift());
    return total;
}

Use it like

addbits('5 + 30 - 25.1 + 11');

Update

Here's a more optimised version

function addbits(s) {
    return (s.replace(/\s/g, '').match(/[+\-]?([0-9\.]+)/g) || [])
        .reduce(function(sum, value) {
            return parseFloat(sum) + parseFloat(value);
        });
}
Stefan Gabos
  • 1,345
  • 13
  • 15
10

I created BigEval for the same purpose.
In solving expressions, it performs exactly same as Eval() and supports operators like %, ^, &, ** (power) and ! (factorial). You are also allowed to use functions and constants (or say variables) inside the expression. The expression is solved in PEMDAS order which is common in programming languages including JavaScript.

var Obj = new BigEval();
var result = Obj.exec("5! + 6.6e3 * (PI + E)"); // 38795.17158152233
var result2 = Obj.exec("sin(45 * deg)**2 + cos(pi / 4)**2"); // 1
var result3 = Obj.exec("0 & -7 ^ -7 - 0%1 + 6%2"); //-7

It can also be made to use those Big Number libraries for arithmetic in case you are dealing with numbers with arbitrary precision.

Avi
  • 1,341
  • 1
  • 18
  • 28
9

I went looking for JavaScript libraries for evaluating mathematical expressions, and found these two promising candidates:

  • JavaScript Expression Evaluator: Smaller and hopefully more light-weight. Allows algebraic expressions, substitutions and a number of functions.

  • mathjs: Allows complex numbers, matrices and units as well. Built to be used by both in-browser JavaScript and Node.js.

Itangalo
  • 325
  • 2
  • 7
  • I've now tested the JavaScript Expression Evaluator, and it seems to rock. (mathjs probably rocks too, but it seems a bit too big for my purposes and I also like the substitution functionality in JSEE.) – Itangalo Feb 12 '14 at 13:20
8

This is a little function I threw together just now to solve this issue - it builds the expression by analyzing the string one character at a time (it's actually pretty quick though). This will take any mathematical expression (limited to +,-,*,/ operators only) and return the result. It can handle negative values and unlimited number operations as well.

The only "to do" left is to make sure it calculates * & / before + & -. Will add that functionality later, but for now this does what I need...

/**
* Evaluate a mathematical expression (as a string) and return the result
* @param {String} expr A mathematical expression
* @returns {Decimal} Result of the mathematical expression
* @example
*    // Returns -81.4600
*    expr("10.04+9.5-1+-100");
*/ 
function expr (expr) {

    var chars = expr.split("");
    var n = [], op = [], index = 0, oplast = true;

    n[index] = "";

    // Parse the expression
    for (var c = 0; c < chars.length; c++) {

        if (isNaN(parseInt(chars[c])) && chars[c] !== "." && !oplast) {
            op[index] = chars[c];
            index++;
            n[index] = "";
            oplast = true;
        } else {
            n[index] += chars[c];
            oplast = false;
        }
    }

    // Calculate the expression
    expr = parseFloat(n[0]);
    for (var o = 0; o < op.length; o++) {
        var num = parseFloat(n[o + 1]);
        switch (op[o]) {
            case "+":
                expr = expr + num;
                break;
            case "-":
                expr = expr - num;
                break;
            case "*":
                expr = expr * num;
                break;
            case "/":
                expr = expr / num;
                break;
        }
    }

    return expr;
}
jMichael
  • 81
  • 1
  • 1
8

I've recently done this in C# (no Eval() for us...) by evaluating the expression in Reverse Polish Notation (that's the easy bit). The hard part is actually parsing the string and turning it into Reverse Polish Notation. I used the Shunting Yard algorithm, as there's a great example on Wikipedia and pseudocode. I found it really simple to implement both and I'd recommend this if you haven't already found a solution or are looking for alternatives.

fwoosh
  • 184
  • 3
  • 15
RichK
  • 11,318
  • 6
  • 35
  • 49
4

You could use a for loop to check if the string contains any invalid characters and then use a try...catch with eval to check if the calculation throws an error like eval("2++") would.

function evaluateMath(str) {
  for (var i = 0; i < str.length; i++) {
    if (isNaN(str[i]) && !['+', '-', '/', '*', '%', '**'].includes(str[i])) {
      return NaN;
    }
  }
  
  
  try {
    return eval(str)
  } catch (e) {
    if (e.name !== 'SyntaxError') throw e
    return NaN;
  }
}

console.log(evaluateMath('2 + 6'))

or instead of a function, you could set Math.eval

Math.eval = function(str) {
  for (var i = 0; i < str.length; i++) {
    if (isNaN(str[i]) && !['+', '-', '/', '*', '%', '**'].includes(str[i])) {
      return NaN;
    }
  }
  
  
  try {
    return eval(str)
  } catch (e) {
    if (e.name !== 'SyntaxError') throw e
    return NaN;
  }
}

console.log(Math.eval('2 + 6'))
shreyasm-dev
  • 2,711
  • 5
  • 16
  • 34
3

I've eventually gone for this solution, which works for summing positive and negative integers (and with a little modification to the regex will work for decimals too):

function sum(string) {
  return (string.match(/^(-?\d+)(\+-?\d+)*$/)) ? string.split('+').stringSum() : NaN;
}   

Array.prototype.stringSum = function() {
    var sum = 0;
    for(var k=0, kl=this.length;k<kl;k++)
    {
        sum += +this[k];
    }
    return sum;
}

I'm not sure if it's faster than eval(), but as I have to carry out the operation lots of times I'm far more comfortable runing this script than creating loads of instances of the javascript compiler

wheresrhys
  • 22,558
  • 19
  • 94
  • 162
  • 1
    Although `return` cannot be used inside an expression, `sum("+1")` returns *NaN*. – Gumbo Mar 06 '10 at 11:54
  • Always foregt whether return has to or can't go inside a ternary expression. I'd like to exclude "+1" because although it 'should' evaluate as a number, it's not really an example of a mathematical sum in the everyday sense. My code is designed to both evaluate and filter for allowable strings. – wheresrhys Mar 06 '10 at 12:12
3

Try nerdamer

var result = nerdamer('12+2+PI').evaluate();
document.getElementById('text').innerHTML = result.text();
<script src="http://nerdamer.com/js/nerdamer.core.js"></script>
<div id="text"></div>
ArchHaskeller
  • 1,270
  • 1
  • 12
  • 28
2

I believe that parseInt and ES6 can be helpful in this situation

let func = (str) => {
  let arr = str.split("");
  return `${Number(arr[0]) + parseInt(arr[1] + Number(arr[2]))}`
};

console.log(func("1+1"));

The main thing here is that parseInt parses the number with the operator. Code can be modified to the corresponding needs.

entithat
  • 324
  • 6
  • 18
fuser
  • 283
  • 2
  • 7
  • 18
2

You can try using the function constructor:

function parse(data) {
    return new Function(` return ${data}`)();
}


parse('1+1')
user3311145
  • 21
  • 1
  • 3
  • Welcome to SO! Be sure to double-check the formatting rules - initially your answer was unreadable because you did not indent the code block. – Dan R Aug 09 '22 at 02:10
1

Try AutoCalculator https://github.com/JavscriptLab/autocalculate Calculate Inputs value and Output By using selector expressions

Just add an attribute for your output input like data-ac="(#firstinput+#secondinput)"

No Need of any initialization just add data-ac attribute only. It will find out dynamically added elements automatically

FOr add 'Rs' with Output just add inside curly bracket data-ac="{Rs}(#firstinput+#secondinput)"

Justine Jose
  • 130
  • 2
  • 7
1
const operatorToFunction = {
    "+": (num1, num2) => +num1 + +num2,
    "-": (num1, num2) => +num1 - +num2,
    "*": (num1, num2) => +num1 * +num2,
    "/": (num1, num2) => +num1 / +num2
}

const findOperator = (str) => {
    const [operator] = str.split("").filter((ch) => ["+", "-", "*", "/"].includes(ch))
    return operator;
}

const executeOperation = (str) => {
    const operationStr = str.replace(/[ ]/g, "");
    const operator = findOperator(operationStr);
    const [num1, num2] = operationStr.split(operator)
    return operatorToFunction[operator](num1, num2);
};

const addition = executeOperation('1 + 1'); // ans is 2
const subtraction = executeOperation('4 - 1'); // ans is 3
const multiplication = executeOperation('2 * 5'); // ans is 10
const division = executeOperation('16 / 4'); // ans is 4
  • 1
    What about subtraction, multiplication, and division? Why multiply `num` by 1? – nathanfranke Mar 23 '20 at 10:03
  • Thank you for pointing it out @nathanfranke I've updated the answer in order to make it more generic. Now it supports all 4 operations. And multiple by 1 was to convert it from string to number. Which we can achieve by doing +num as well. – Rushikesh Bharad Mar 24 '20 at 13:29
1

eval was far too slow for me. So I developed an StringMathEvaluator(SME), that follows the order of operations and works for all arithmetic equations containing the following:

  • Integers
  • Decimals
  • Mathematical Operators: +-*/
  • Preferential Perenthesis: $operator ($expression) $operator
  • Variables: If and only if you define a global and/or local scope.
    • Format: [a-zA-Z][a-zA-Z0-9]*
    • Nest Variable Operator: $var1.$var2
    • Function Parenthesis: $functionId(...$commaSepArgs)
    • Array Brackets: $arrayId[index]
  • (Ignores Spaces)

Speed Test Results: (Ran within chromium browser)

                                      ~(80 - 99)% faster with reasonable expression complexity.

                     500000 iterations (SME/eval)

Integer Test '4'
(0.346/35.646)Sec - SME 99.03% faster

Simple Equation Test '4+-3'
(0.385/35.09)Sec - SME 98.9% faster

Complex Equation Test '(16 / 44 * 2) + ((4 + (4+3)-(12- 6)) / (2 * 8))'
(2.798/38.116)Sec - SME 92.66% faster

Variable Evaluation Test '2 + 5.5 + Math.round(Math.sqrt(Math.PI)) + values.one + values.two + values.four.nested'
(6.113/38.177)Sec - SME 83.99% faster

Example Usage:

Initialize:

Without Variables:

const math = new StringMathEvaluator();
const twentyOne = math.eval('11 + 10');
console.log('BlackJack' + twentyOne);
// BlackJack21

With Variables

const globalScope = {Math};
const math = new StringMathEvaluator(globalScope);

const localScope = {a: [[1, () => ({func: () => [17,13]})],[11,64,2]]};
const str = '((a[0][1]().func()[0] + a[0][1]().func()[1]) * a[1][2] - Math.sqrt(a[1][1]) - a[1][0]) / a[0][0]';
const fortyOne = math.eval(str, localScope);
console.log('Sum' + fortyOne);
// Sum41

SME:

class StringMathEvaluator {
  constructor(globalScope) {
    globalScope = globalScope || {};
    const instance = this;
    let splitter = '.';

    function resolve (path, currObj, globalCheck) {
      if (path === '') return currObj;
      try {
        if ((typeof path) === 'string') path = path.split(splitter);
        for (let index = 0; index < path.length; index += 1) {
          currObj = currObj[path[index]];
        }
        if (currObj === undefined && !globalCheck) throw Error('try global');
        return currObj;
      }  catch (e) {
        return resolve(path, globalScope, true);
      }
    }

    function multiplyOrDivide (values, operands) {
      const op = operands[operands.length - 1];
      if (op === StringMathEvaluator.multi || op === StringMathEvaluator.div) {
        const len = values.length;
        values[len - 2] = op(values[len - 2], values[len - 1])
        values.pop();
        operands.pop();
      }
    }

    const resolveArguments = (initialChar, func) => {
      return function (expr, index, values, operands, scope, path) {
        if (expr[index] === initialChar) {
          const args = [];
          let endIndex = index += 1;
          const terminationChar = expr[index - 1] === '(' ? ')' : ']';
          let terminate = false;
          let openParenCount = 0;
          while(!terminate && endIndex < expr.length) {
            const currChar = expr[endIndex++];
            if (currChar === '(') openParenCount++;
            else if (openParenCount > 0 && currChar === ')') openParenCount--;
            else if (openParenCount === 0) {
              if (currChar === ',') {
                args.push(expr.substr(index, endIndex - index - 1));
                index = endIndex;
              } else if (openParenCount === 0 && currChar === terminationChar) {
                args.push(expr.substr(index, endIndex++ - index - 1));
                terminate = true;
              }
            }
          }

          for (let index = 0; index < args.length; index += 1) {
            args[index] = instance.eval(args[index], scope);
          }
          const state = func(expr, path, scope, args, endIndex);
          if (state) {
            values.push(state.value);
            return state.endIndex;
          }
        }
      }
    };

    function chainedExpressions(expr, value, endIndex, path) {
      if (expr.length === endIndex) return {value, endIndex};
      let values = [];
      let offsetIndex;
      let valueIndex = 0;
      let chained = false;
      do {
        const subStr = expr.substr(endIndex);
        const offsetIndex = isolateArray(subStr, 0, values, [], value, path) ||
                            isolateFunction(subStr, 0, values, [], value, path) ||
                            (subStr[0] === '.' &&
                              isolateVar(subStr, 1, values, [], value));
        if (Number.isInteger(offsetIndex)) {
          value = values[valueIndex];
          endIndex += offsetIndex - 1;
          chained = true;
        }
      } while (offsetIndex !== undefined);
      return {value, endIndex};
    }

    const isolateArray = resolveArguments('[',
      (expr, path, scope, args, endIndex) => {
        endIndex = endIndex - 1;
        let value = resolve(path, scope)[args[args.length - 1]];
        return chainedExpressions(expr, value, endIndex, '');
      });

    const isolateFunction = resolveArguments('(',
      (expr, path, scope, args, endIndex) =>
          chainedExpressions(expr, resolve(path, scope).apply(null, args), endIndex - 1, ''));

    function isolateParenthesis(expr, index, values, operands, scope) {
      const char = expr[index];
      if (char === '(') {
        let openParenCount = 1;
        let endIndex = index + 1;
        while(openParenCount > 0 && endIndex < expr.length) {
          const currChar = expr[endIndex++];
          if (currChar === '(') openParenCount++;
          if (currChar === ')') openParenCount--;
        }
        const len = endIndex - index - 2;
        values.push(instance.eval(expr.substr(index + 1, len), scope));
        multiplyOrDivide(values, operands);
        return endIndex;
      }
    };

    function isolateOperand (char, operands) {
      switch (char) {
        case '*':
        operands.push(StringMathEvaluator.multi);
        return true;
        break;
        case '/':
        operands.push(StringMathEvaluator.div);
        return true;
        break;
        case '+':
        operands.push(StringMathEvaluator.add);
        return true;
        break;
        case '-':
        operands.push(StringMathEvaluator.sub);
        return true;
        break;
      }
      return false;
    }

    function isolateValueReg(reg, resolver, splitter) {
      return function (expr, index, values, operands, scope) {
        const match = expr.substr(index).match(reg);
        let args;
        if (match) {
          let endIndex = index + match[0].length;
          let value = resolver(match[0], scope);
          if (!Number.isFinite(value)) {
            const state = chainedExpressions(expr, scope, endIndex, match[0]);
            if (state !== undefined) {
              value = state.value;
              endIndex = state.endIndex;
            }
          }
          values.push(value);
          multiplyOrDivide(values, operands);
          return endIndex;
        }
      }
    }
    const isolateNumber = isolateValueReg(StringMathEvaluator.numReg, Number.parseFloat);
    const isolateVar = isolateValueReg(StringMathEvaluator.varReg, resolve);


    this.eval = function (expr, scope) {
      scope = scope || globalScope;
      const allowVars = (typeof scope) === 'object';
      let operands = [];
      let values = [];
      let prevWasOpperand = true;
      for (let index = 0; index < expr.length; index += 1) {
        const char = expr[index];
        if (prevWasOpperand) {
          let newIndex = isolateParenthesis(expr, index, values, operands, scope) ||
                        isolateNumber(expr, index, values, operands, scope) ||
                        (allowVars && isolateVar(expr, index, values, operands, scope));
          if (Number.isInteger(newIndex)) {
            index = newIndex - 1;
            prevWasOpperand = false;
          }
        } else {
          prevWasOpperand = isolateOperand(char, operands);
        }
      }
      let value = values[0];
      for (let index = 0; index < values.length - 1; index += 1) {
        value = operands[index](values[index], values[index + 1]);
        values[index + 1] = value;
      }
      return value;
    }
  }
}

StringMathEvaluator.numReg = /^(-|)[0-9\.]{1,}/;
StringMathEvaluator.varReg = /^((\.|)([a-zA-Z][a-zA-Z0-9\.]*))/;
StringMathEvaluator.multi = (n1, n2) => n1 * n2;
StringMathEvaluator.div = (n1, n2) => n1 / n2;
StringMathEvaluator.add = (n1, n2) => n1 + n2;
StringMathEvaluator.sub = (n1, n2) => n1 - n2;
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
1

The best way and easiest way is to use math.js library. Here some example code demonstrating how to use the library. Click here to fiddle around.

// functions and constants
math.round(math.e, 3)                // 2.718
math.atan2(3, -3) / math.pi          // 0.75
math.log(10000, 10)                  // 4
math.sqrt(-4)                        // 2i
math.derivative('x^2 + x', 'x')      // 2*x+1
math.pow([[-1, 2], [3, 1]], 2)
     // [[7, 0], [0, 7]]

// expressions
math.evaluate('1.2 * (2 + 4.5)')     // 7.8
math.evaluate('12.7 cm to inch')     // 5 inch
math.evaluate('sin(45 deg) ^ 2')     // 0.5
math.evaluate('9 / 3 + 2i')          // 3 + 2i
math.evaluate('det([-1, 2; 3, 1])')  // -7

// chaining
math.chain(3)
    .add(4)
    .multiply(2)
    .done() // 14
Salim Hamidi
  • 20,731
  • 1
  • 26
  • 31
1

I made a small function to parse a math expression, containing +,/,-,*. I used if statements I think switch cases will be better. Firstly I separated the string into the operator and its numbers convert then from string to float then iterate through while performing the operation.

 const evaluate=(mathExpStr) => {
    mathExpStr.replace(/[+-\/*]$/, "");
    let regExp = /\d+/g;
    let valueArr = (mathExpStr.match(regExp) || []).map((val) =>
      Number.parseFloat(val)
    );
    let operatorArr = mathExpStr.match(/[+-\/*]/g) || [];
    return converter(valueArr, operatorArr)
  }

const converter = (arr,operators)=>{
  let arr2=[...arr]
  for(let i=0;i<arr.length;i++){
    let o;
    if(arr2.length<2){return arr2[0]}
    if(operators[i]=="+"){
      o=arr2[0]+arr2[1]
      arr2.splice(0, 2, o)
      console.log(o,arr2, operators[i])
    }
    if(operators[i]=="-"){
      o=arr2[0]-arr2[1]
      arr2.splice(0,2, o)
      console.log(o,arr2, operators[i])
    }
    if(operators[i]=="*"){
      o=arr2[0]*arr2[1]
      arr2.splice(0,2,o)
      console.log(o,arr2, operators[i])
    }
    if(operators[i]=="/"){
      o=arr2[0]/arr2[1]
      arr2.splice(0,2, o)
      console.log(o,arr2, operators[i])
    }
  }
}
// console.log(converter(valueArr, operatorArr))
console.log(evaluate("1+3+5+6-4*2/4"))
Yagi91
  • 35
  • 6
1

Current answers are either:

  • suggesting huge external libraries
  • using eval('...') or Function('...') which is bad
  • working only with +, -
  • failing at operator precedence (e.g 1+2*3 should return 7 not 9) when they implement * and/or /

Here an implementation (270 lines of code) that replaces eval.

It supports +, -, *, /, %, ^, parentheses and functions (min, max, sin, cos, tan, log). You can also easily add support for more functions like sqrt, asin, acos...)

It uses TypeScript, is documented and tested.

It internally uses the shunting yard algorithm and reverse Polish notation.

// WTF!
// parseFloat('-0') => -0 vs parseFloat(-0) => 0
// -0 === 0 => true vs Object.is(-0, 0) => false
const minus0Hack = (value: number) => (Object.is(value, -0) ? '-0' : value);

export const operators: {
  [operator: string]:
    | {
        func: (...args: string[]) => string;
        precedence: number;
        associativity: 'left' | 'right';
        arity: number; // Needed by evalReversePolishNotation()
      }
    | undefined;
} = {
  '+': {
    func: (x, y) => `${minus0Hack(Number(x) + Number(y))}`,
    precedence: 1,
    associativity: 'left',
    arity: 2
  },
  '-': {
    func: (x, y) => `${minus0Hack(Number(x) - Number(y))}`,
    precedence: 1,
    associativity: 'left',
    arity: 2
  },
  '*': {
    func: (x, y) => `${minus0Hack(Number(x) * Number(y))}`,
    precedence: 2,
    associativity: 'left',
    arity: 2
  },
  '/': {
    func: (x, y) => `${minus0Hack(Number(x) / Number(y))}`,
    precedence: 2,
    associativity: 'left',
    arity: 2
  },
  '%': {
    func: (x, y) => `${minus0Hack(Number(x) % Number(y))}`,
    precedence: 2,
    associativity: 'left',
    arity: 2
  },
  '^': {
    // Why Math.pow() instead of **?
    // -2 ** 2 => "SyntaxError: Unary operator used immediately before exponentiation expression..."
    // Math.pow(-2, 2) => -4
    // eslint-disable-next-line prefer-exponentiation-operator, no-restricted-properties
    func: (x, y) => `${minus0Hack(Math.pow(Number(x), Number(y)))}`,
    precedence: 3,
    associativity: 'right',
    arity: 2
  }
};
export const operatorsKeys = Object.keys(operators);

export const functions: {
  [operator: string]:
    | {
        func: (...args: string[]) => string;
        // Needed by evalReversePolishNotation()
        arity: number;
      }
    | undefined;
} = {
  min: { func: (x, y) => `${minus0Hack(Math.min(Number(x), Number(y)))}`, arity: 2 },
  max: { func: (x, y) => `${minus0Hack(Math.max(Number(x), Number(y)))}`, arity: 2 },
  sin: { func: x => `${minus0Hack(Math.sin(Number(x)))}`, arity: 1 },
  cos: { func: x => `${minus0Hack(Math.cos(Number(x)))}`, arity: 1 },
  tan: { func: x => `${minus0Hack(Math.tan(Number(x)))}`, arity: 1 },
  log: { func: x => `${Math.log(Number(x))}`, arity: 1 } // No need for -0 hack
};
export const functionsKeys = Object.keys(functions);

const top = (stack: string[]): string | undefined => stack[stack.length - 1];

/**
 * Shunting yard algorithm: converts infix expression to postfix expression (reverse Polish notation)
 *
 * Example: ['1', '+', '2'] => ['1', '2', '+']
 *
 * https://en.wikipedia.org/wiki/Shunting_yard_algorithm
 * https://github.com/poteat/shunting-yard-typescript
 * https://blog.kallisti.net.nz/2008/02/extension-to-the-shunting-yard-algorithm-to-allow-variable-numbers-of-arguments-to-functions/
 */
export function shuntingYard(tokens: string[]) {
  const output = new Array<string>();
  const operatorStack = new Array<string>();

  for (const token of tokens) {
    if (functions[token] !== undefined) {
      operatorStack.push(token);
    } else if (token === ',') {
      while (operatorStack.length > 0 && top(operatorStack) !== '(') {
        output.push(operatorStack.pop()!);
      }
      if (operatorStack.length === 0) {
        throw new Error("Misplaced ','");
      }
    } else if (operators[token] !== undefined) {
      const o1 = token;
      while (
        operatorStack.length > 0 &&
        top(operatorStack) !== undefined &&
        top(operatorStack) !== '(' &&
        (operators[top(operatorStack)!]!.precedence > operators[o1]!.precedence ||
          (operators[o1]!.precedence === operators[top(operatorStack)!]!.precedence &&
            operators[o1]!.associativity === 'left'))
      ) {
        output.push(operatorStack.pop()!); // o2
      }
      operatorStack.push(o1);
    } else if (token === '(') {
      operatorStack.push(token);
    } else if (token === ')') {
      while (operatorStack.length > 0 && top(operatorStack) !== '(') {
        output.push(operatorStack.pop()!);
      }
      if (operatorStack.length > 0 && top(operatorStack) === '(') {
        operatorStack.pop();
      } else {
        throw new Error('Parentheses mismatch');
      }
      if (functions[top(operatorStack)!] !== undefined) {
        output.push(operatorStack.pop()!);
      }
    } else {
      output.push(token);
    }
  }

  // Remaining items
  while (operatorStack.length > 0) {
    const operator = top(operatorStack);
    if (operator === '(') {
      throw new Error('Parentheses mismatch');
    } else {
      output.push(operatorStack.pop()!);
    }
  }

  return output;
}

/**
 * Evaluates reverse Polish notation (RPN) (postfix expression).
 *
 * Example: ['1', '2', '+'] => 3
 *
 * https://en.wikipedia.org/wiki/Reverse_Polish_notation
 * https://github.com/poteat/shunting-yard-typescript
 */
export function evalReversePolishNotation(tokens: string[]) {
  const stack = new Array<string>();

  const ops = { ...operators, ...functions };

  for (const token of tokens) {
    const op = ops[token];

    // eslint-disable-next-line unicorn/no-negated-condition
    if (op !== undefined) {
      const parameters = [];
      for (let i = 0; i < op.arity; i++) {
        parameters.push(stack.pop()!);
      }
      stack.push(op.func(...parameters.reverse()));
    } else {
      stack.push(token);
    }
  }

  if (stack.length > 1) {
    throw new Error('Insufficient operators');
  }

  return Number(stack[0]);
}

/**
 * Breaks a mathematical expression into tokens.
 *
 * Example: "1 + 2" => [1, '+', 2]
 *
 * https://gist.github.com/tchayen/44c28e8d4230b3b05e9f
 */
export function tokenize(expression: string) {
  // "1  +" => "1 +"
  const expr = expression.replace(/\s+/g, ' ');

  const tokens = [];

  let acc = '';
  let currentNumber = '';

  for (let i = 0; i < expr.length; i++) {
    const c = expr.charAt(i);
    const prev_c = expr.charAt(i - 1); // '' if index out of range
    const next_c = expr.charAt(i + 1); // '' if index out of range

    const lastToken = top(tokens);

    const numberParsingStarted = currentNumber !== '';

    if (
      // 1
      /\d/.test(c) ||
      // Unary operator: +1 or -1
      ((c === '+' || c === '-') &&
        !numberParsingStarted &&
        (lastToken === undefined ||
          lastToken === ',' ||
          lastToken === '(' ||
          operatorsKeys.includes(lastToken)) &&
        /\d/.test(next_c))
    ) {
      currentNumber += c;
    } else if (c === '.') {
      if (numberParsingStarted && currentNumber.includes('.')) {
        throw new Error(`Double '.' in number: '${currentNumber}${c}'`);
      } else {
        currentNumber += c;
      }
    } else if (c === ' ') {
      if (/\d/.test(prev_c) && /\d/.test(next_c)) {
        throw new Error(`Space in number: '${currentNumber}${c}${next_c}'`);
      }
    } else if (functionsKeys.includes(acc + c)) {
      acc += c;
      if (!functionsKeys.includes(acc + next_c)) {
        tokens.push(acc);
        acc = '';
      }
    } else if (operatorsKeys.includes(c) || c === '(' || c === ')' || c === ',') {
      if (
        operatorsKeys.includes(c) &&
        !numberParsingStarted &&
        operatorsKeys.includes(lastToken!)
      ) {
        throw new Error(`Consecutive operators: '${lastToken!}${c}'`);
      }
      if (numberParsingStarted) {
        tokens.push(currentNumber);
      }
      tokens.push(c);
      currentNumber = '';
    } else {
      acc += c;
    }
  }

  if (acc !== '') {
    throw new Error(`Invalid characters: '${acc}'`);
  }

  // Add last number to the tokens
  if (currentNumber !== '') {
    tokens.push(currentNumber);
  }

  // ['+', '1'] => ['0', '+', '1']
  // ['-', '1'] => ['0', '-', '1']
  if (tokens[0] === '+' || tokens[0] === '-') {
    tokens.unshift('0');
  }

  return tokens;
}

export function calculate(expression: string) {
  const tokens = tokenize(expression);
  const rpn = shuntingYard(tokens);
  return evalReversePolishNotation(rpn);
}

The most important part, the unit tests:

/* eslint-disable no-eval, unicorn/prefer-number-properties */

import { calculate, evalReversePolishNotation, shuntingYard, tokenize } from './calculator';
import { convertMathExpressionToEval, getRandomMathExpression } from './getRandomMathExpression';
import { getRandomInt } from './getRandomNumber';

test('shuntingYard()', () => {
  {
    // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
    const rpn = shuntingYard('3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3'.split(' '));
    expect(rpn).toEqual(['3', '4', '2', '*', '1', '5', '-', '2', '3', '^', '^', '/', '+']);
  }

  {
    // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
    const rpn = shuntingYard('sin ( max ( 2 3 ) / 3 * 3.14 )'.split(' '));
    expect(rpn).toEqual(['2', '3', 'max', '3', '/', '3.14', '*', 'sin']);
  }

  // Parentheses mismatch
  expect(() => shuntingYard(['('])).toThrow('Parentheses mismatch');
  expect(() => shuntingYard([')'])).toThrow('Parentheses mismatch');
  expect(() => shuntingYard('1 - ( 2 * 3 ) )'.split(' '))).toThrow('Parentheses mismatch');
  expect(() => shuntingYard('1 - ( 2 * 3 ) ) + 4'.split(' '))).toThrow('Parentheses mismatch');

  // Ignore ','
  expect(shuntingYard('max ( 1 , 2 )'.split(' '))).toEqual(['1', '2', 'max']);

  // if the token is: ',':
  //   while the operator at the top of the operator stack is not a left parenthesis:
  //     pop the operator from the operator stack into the output queue
  expect(shuntingYard('max ( 0 + 1 , 2 )'.split(' '))).toEqual(['0', '1', '+', '2', 'max']);

  // Misplaced ','
  expect(() => shuntingYard('1 , 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard(', 1 / 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard('1 , / 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard('1 / , 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard('1 / 2 ,'.split(' '))).toThrow("Misplaced ','");
  expect(() =>
    shuntingYard('sin ( , max , ( , 2 , 3 , ) , / , 3 , * , 3.14 , )'.split(' '))
  ).not.toThrow();

  // Edge cases
  expect(shuntingYard([''])).toEqual(['']);
  expect(shuntingYard([' '])).toEqual([' ']);
  expect(shuntingYard(['1'])).toEqual(['1']);
  expect(shuntingYard(['a'])).toEqual(['a']);
  expect(shuntingYard(['1a'])).toEqual(['1a']);
  expect(shuntingYard(['*'])).toEqual(['*']);
  expect(shuntingYard(['/'])).toEqual(['/']);

  // All together expression
  expect(
    shuntingYard(
      '( ( 3.1 + cos ( -4 ) / 2 ) * max ( -6 , 6 ) ^ sin ( 6 ) * 9 ) / tan ( log ( 8.8 + -2 ) % 7 ) + ( 6 * -1 - min ( 6 , -4.2 ) )'.split(
        ' '
      )
    )
  ).toEqual(
    '3.1 -4 cos 2 / + -6 6 max 6 sin ^ * 9 * 8.8 -2 + log 7 % tan / 6 -1 * 6 -4.2 min - +'.split(
      ' '
    )
  );
});

test('reversePolishNotation()', () => {
  // https://rosettacode.org/wiki/Parsing/RPN_calculator_algorithm#JavaScript
  expect(
    evalReversePolishNotation(['3', '4', '2', '*', '1', '5', '-', '2', '3', '^', '^', '/', '+'])
  ).toEqual(3 + (4 * 2) / (1 - 5) ** (2 ** 3));
  expect(
    evalReversePolishNotation(['3', '4', '2', '*', '1', '5', '-', '2', '3', '^', '^', '/', '+'])
  ).toEqual(3.000_122_070_312_5);

  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(evalReversePolishNotation(['2', '3', 'max', '3', '/', '3.14', '*', 'sin'])).toEqual(
    Math.sin((Math.max(2, 3) / 3) * 3.14)
  );
  expect(evalReversePolishNotation(['2', '3', 'max', '3', '/', '3.14', '*', 'sin'])).toEqual(
    0.001_592_652_916_486_828_2
  );

  // Edge cases
  expect(evalReversePolishNotation([''])).toEqual(0); // :-(
  expect(evalReversePolishNotation([' '])).toEqual(0); // :-(
  expect(evalReversePolishNotation(['1'])).toEqual(1);
  expect(evalReversePolishNotation(['a'])).toBeNaN();
  expect(evalReversePolishNotation(['1a'])).toBeNaN();
  expect(evalReversePolishNotation(['*'])).toBeNaN();
  expect(evalReversePolishNotation(['/'])).toBeNaN();
  expect(() => evalReversePolishNotation(['1', '2'])).toThrow('Insufficient operators');

  // All together expression
  expect(
    evalReversePolishNotation(
      '3.1 -4 cos 2 / + -6 6 max 6 sin ^ * 9 * 8.8 -2 + log 7 % tan / 6 -1 * 6 -4.2 min - +'.split(
        ' '
      )
    )
  ).toEqual(
    eval(
      '((3.1 + Math.cos(-4) / 2) * Math.max(-6, 6) ** Math.sin(6) * 9) / Math.tan(Math.log(8.8 + -2) % 7) + (6 * -1 - Math.min(6, -4.2))'
    )
  );
});

test('tokenize()', () => {
  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(tokenize('3 + 4 * 2 / (1 - 5) ^ 2 ^ 3')).toEqual(
    '3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3'.split(' ')
  );

  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(tokenize('sin(max(2, 3) / 3 * 3.14)')).toEqual(
    'sin ( max ( 2 , 3 ) / 3 * 3.14 )'.split(' ')
  );

  expect(tokenize('1+2')).toEqual(['1', '+', '2']);
  expect(tokenize('min(1,2)')).toEqual(['min', '(', '1', ',', '2', ')']);
  expect(tokenize('1.1+2.2')).toEqual(['1.1', '+', '2.2']);
  expect(tokenize('min(1.1,2.2)')).toEqual(['min', '(', '1.1', ',', '2.2', ')']);

  // Decimals
  expect(tokenize('1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ^ 7.7')).toEqual(
    '1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ^ 7.7'.split(' ')
  );

  // White spaces
  expect(tokenize('')).toEqual([]);
  expect(tokenize(' ')).toEqual([]);
  expect(tokenize(' 1  +  2 ')).toEqual(['1', '+', '2']);
  expect(tokenize('1 \n + \n 2')).toEqual(['1', '+', '2']);
  expect(tokenize('1 \t + \t 2')).toEqual(['1', '+', '2']);

  // Single number
  expect(tokenize('0')).toEqual(['0']);
  expect(tokenize('1')).toEqual(['1']);
  expect(tokenize('-0')).toEqual(['-0']);
  expect(tokenize('-1')).toEqual(['-1']);
  expect(tokenize('(1)')).toEqual(['(', '1', ')']);
  expect(tokenize('(-1)')).toEqual(['(', '-1', ')']);
  expect(tokenize('-(1)')).toEqual(['0', '-', '(', '1', ')']);

  // Starting with +/-
  expect(tokenize('+0')).toEqual(['+0']);
  expect(tokenize('+ 0')).toEqual(['0', '+', '0']);
  expect(tokenize('-0')).toEqual(['-0']);
  expect(tokenize('- 0')).toEqual(['0', '-', '0']);
  expect(tokenize('+1')).toEqual(['+1']);
  expect(tokenize('+ 1')).toEqual(['0', '+', '1']);
  expect(tokenize('-1')).toEqual(['-1']);
  expect(tokenize('- 1')).toEqual(['0', '-', '1']);
  expect(tokenize('+1 + 1')).toEqual(['+1', '+', '1']);
  expect(tokenize('+ 1 + 1')).toEqual(['0', '+', '1', '+', '1']);
  expect(tokenize('-1 + 1')).toEqual(['-1', '+', '1']);
  expect(tokenize('- 1 + 1')).toEqual(['0', '-', '1', '+', '1']);
  expect(tokenize('+')).toEqual(['0', '+']);
  expect(tokenize('-')).toEqual(['0', '-']);

  // Do not confuse '+1' / '-1' with 'x + 1' / 'x - 1' depending on the context
  expect(tokenize('(1+2)+1')).toEqual(['(', '1', '+', '2', ')', '+', '1']);
  expect(tokenize('(1+2)-1')).toEqual(['(', '1', '+', '2', ')', '-', '1']);
  expect(tokenize('1 + -2')).toEqual(['1', '+', '-2']);
  expect(tokenize('1+-2')).toEqual(['1', '+', '-2']);

  // Space in number
  expect(() => tokenize('1 2')).toThrow("Space in number: '1 2'");
  expect(() => tokenize('1  2')).toThrow("Space in number: '1 2'");
  expect(() => tokenize('0 + 1 / (2 3) * 4')).toThrow("Space in number: '2 3'");
  expect(() => tokenize('min(1 2)')).toThrow("Space in number: '1 2'");

  // Double '.' in number
  expect(() => tokenize('1+2.3.4')).toThrow("Double '.' in number: '2.3.'");
  expect(() => tokenize('1+2.3.4.5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => tokenize('0 + 1 / 2.3.4 * 5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => tokenize('min(1, 2.3.4)')).toThrow("Double '.' in number: '2.3.'");

  // Consecutive operators
  expect(tokenize('1++2')).toEqual(['1', '+', '+2']);
  expect(tokenize('1-+2')).toEqual(['1', '-', '+2']);
  expect(tokenize('1--2')).toEqual(['1', '-', '-2']);
  expect(() => tokenize('1++')).toThrow("Consecutive operators: '++'");
  expect(() => tokenize('1-+')).toThrow("Consecutive operators: '-+'");
  expect(() => tokenize('1--')).toThrow("Consecutive operators: '--'");
  expect(() => tokenize('1-*2')).toThrow("Consecutive operators: '-*'");
  expect(() => tokenize('0 + 1 / (2-*3) * 4')).toThrow("Consecutive operators: '-*'");
  expect(() => tokenize('min(1-*2, 3)')).toThrow("Consecutive operators: '-*'");

  // Other edge cases
  expect(tokenize('1,2')).toEqual(['1', ',', '2']);
  expect(tokenize('1+2+')).toEqual(['1', '+', '2', '+']); // :-(
  expect(() => tokenize('1+2a')).toThrow("Invalid characters: 'a'");
  expect(() => tokenize('10 Hello')).toThrow("Invalid characters: 'Hello'");
  expect(tokenize('1-.')).toEqual(['1', '-', '.']); // :-(
  expect(tokenize('*')).toEqual(['*']);
  expect(tokenize('/')).toEqual(['/']);

  // All together expression
  expect(
    tokenize(
      '((3.1 + cos(-4) / 2) * max(-6, 6) ^ sin(6) * 9) / tan(log(8.8 + -2) % 7) + (6 * -1 - min(6, -4.2))'
    )
  ).toEqual(
    '( ( 3.1 + cos ( -4 ) / 2 ) * max ( -6 , 6 ) ^ sin ( 6 ) * 9 ) / tan ( log ( 8.8 + -2 ) % 7 ) + ( 6 * -1 - min ( 6 , -4.2 ) )'.split(
      ' '
    )
  );
  expect(
    tokenize('((3.1+cos(-4)/2)*max(-6,6)^sin(6)*9)/tan(log(8.8+-2)%7)+(6*-1-min(6,-4.2))')
  ).toEqual(
    '( ( 3.1 + cos ( -4 ) / 2 ) * max ( -6 , 6 ) ^ sin ( 6 ) * 9 ) / tan ( log ( 8.8 + -2 ) % 7 ) + ( 6 * -1 - min ( 6 , -4.2 ) )'.split(
      ' '
    )
  );
});

test('calculate()', () => {
  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(calculate('3 + 4 * 2 / (1 - 5) ^ 2 ^ 3')).toEqual(3.000_122_070_312_5);

  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(calculate('sin(max(2, 3) / 3 * 3.14)')).toEqual(0.001_592_652_916_486_828_2);

  expect(calculate('1+2')).toEqual(3);
  expect(calculate('min(1,2)')).toEqual(1);
  expect(calculate('1.1+2.2')).toEqual(3.300_000_000_000_000_3);
  expect(calculate('min(1.1,2.2)')).toEqual(1.1);

  // if the token is: ',':
  //   while the operator at the top of the operator stack is not a left parenthesis:
  //     pop the operator from the operator stack into the output queue
  expect(calculate('max(0 + 1, 2)')).toEqual(2);

  // Decimals
  expect(calculate('1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ^ 7.7')).toEqual(
    eval('1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ** 7.7')
  );

  // White spaces
  expect(calculate('')).toBeNaN();
  expect(calculate(' ')).toBeNaN();
  expect(calculate(' 1  +  2 ')).toEqual(3);
  expect(calculate('1 \n + \n 2')).toEqual(3);
  expect(calculate('1 \t + \t 2')).toEqual(3);

  // -0 hack
  expect(calculate('-0 + -0')).toEqual(-0);
  expect(calculate('-0 - 0')).toEqual(-0);
  expect(calculate('0 * -1')).toEqual(-0);
  expect(calculate('0 / -1')).toEqual(-0);
  expect(calculate('-1 % 1')).toEqual(-0);
  expect(calculate('-0 ^ 1')).toEqual(-0);
  expect(calculate('min(-0, 1)')).toEqual(-0);
  expect(calculate('max(-0, -1)')).toEqual(-0);
  expect(calculate('sin(-0)')).toEqual(-0);
  //expect(Math.cos(Math.PI / 2)).toEqual(0);
  expect(calculate('tan(-0)')).toEqual(-0);
  expect(calculate('log(1)')).toEqual(0); // No need for -0 hack

  // Math.pow() vs **
  expect(calculate('-2 ^ 2')).toEqual((-2) ** 2);
  expect(eval('Math.pow(-2, 2)')).toEqual(4);
  expect(() => eval('-2 ** 2')).toThrow(
    'Unary operator used immediately before exponentiation expression.'
  );

  // Infinity/-Infinity
  expect(calculate('1 / 0')).toEqual(Infinity);
  expect(calculate('1 / -0')).toEqual(-Infinity);
  expect(calculate('-1 / 0')).toEqual(-Infinity);
  expect(calculate('1 + 1 / 0')).toEqual(Infinity);
  expect(calculate('1 - 1 / 0')).toEqual(-Infinity);
  expect(calculate('10 ^ 1000')).toEqual(Infinity);
  expect(calculate('0 - 10 ^ 1000')).toEqual(-Infinity);
  expect(calculate('0 ^ -1')).toEqual(Infinity);
  expect(calculate('-0 ^ -1')).toEqual(-Infinity);
  expect(calculate('log(0)')).toEqual(-Infinity);

  // NaN
  expect(calculate('log(-1)')).toBeNaN();
  expect(calculate('-1 ^ 0.1')).toBeNaN();
  expect(calculate('1 % 0')).toBeNaN();
  expect(calculate('1 / 0 * 0')).toBeNaN();

  // Single number
  expect(calculate('0')).toEqual(0);
  expect(calculate('1')).toEqual(1);
  expect(calculate('-0')).toEqual(-0);
  expect(calculate('-1')).toEqual(-1);
  expect(calculate('(1)')).toEqual(1);
  expect(calculate('(-1)')).toEqual(-1);
  expect(calculate('-(1)')).toEqual(-1);

  // Starting with +/-
  expect(calculate('+0')).toEqual(0);
  expect(calculate('+ 0')).toEqual(0);
  expect(calculate('-0')).toEqual(-0);
  expect(calculate('- 0')).toEqual(0);
  expect(calculate('+1')).toEqual(1);
  expect(calculate('+ 1')).toEqual(+1);
  expect(calculate('-1')).toEqual(-1);
  expect(calculate('- 1')).toEqual(-1);
  expect(calculate('+1 + 1')).toEqual(2);
  expect(calculate('+ 1 + 1')).toEqual(2);
  expect(calculate('-1 + 1')).toEqual(0);
  expect(calculate('- 1 + 1')).toEqual(0);
  expect(calculate('+')).toBeNaN();
  expect(calculate('-')).toBeNaN();

  // Do not confuse '+1' / '-1' with 'x + 1' / 'x - 1' depending on the context
  expect(calculate('(1+2)+1')).toEqual(4);
  expect(calculate('(1+2)-1')).toEqual(2);
  expect(calculate('1 + -2')).toEqual(-1);
  expect(calculate('1+-2')).toEqual(-1);

  // Space in number
  expect(() => calculate('1 2')).toThrow("Space in number: '1 2'");
  expect(() => calculate('1  2')).toThrow("Space in number: '1 2'");
  expect(() => calculate('0 + 1 / (2 3) * 4')).toThrow("Space in number: '2 3'");
  expect(() => calculate('min(1 2)')).toThrow("Space in number: '1 2'");

  // Double '.' in number
  expect(() => calculate('1+2.3.4')).toThrow("Double '.' in number: '2.3.'");
  expect(() => calculate('1+2.3.4.5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => calculate('0 + 1 / 2.3.4 * 5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => calculate('min(1, 2.3.4)')).toThrow("Double '.' in number: '2.3.'");

  // Consecutive operators
  expect(calculate('1++2')).toEqual(3);
  expect(calculate('1-+2')).toEqual(-1);
  expect(calculate('1--2')).toEqual(3);
  expect(() => calculate('1++')).toThrow("Consecutive operators: '++'");
  expect(() => calculate('1-+')).toThrow("Consecutive operators: '-+'");
  expect(() => calculate('1--')).toThrow("Consecutive operators: '--'");
  expect(() => calculate('1-*2')).toThrow("Consecutive operators: '-*'");
  expect(() => calculate('0 + 1 / (2-*3) * 4')).toThrow("Consecutive operators: '-*'");
  expect(() => calculate('min(1-*2, 3)')).toThrow("Consecutive operators: '-*'");

  // Misplaced ','
  expect(() => calculate('1,2')).toThrow("Misplaced ','");
  expect(() => calculate(',1/2')).toThrow("Misplaced ','");
  expect(() => calculate('1,/2')).toThrow("Misplaced ','");
  expect(() => calculate('1/,2')).toThrow("Misplaced ','");
  expect(() => calculate('1/2,')).toThrow("Misplaced ','");
  expect(() => calculate('sin(,max,(,2,3,),/,3,*,3.14,)')).toThrow('Insufficient operators');
  expect(calculate('sin(,max(,2,3,),/3,*3.14,)')).toEqual(0.001_592_652_916_486_828_2);

  // Other edge cases
  expect(calculate('1+2+')).toBeNaN();
  expect(() => calculate('1+2a')).toThrow("Invalid characters: 'a'");
  expect(() => calculate('10 Hello')).toThrow("Invalid characters: 'Hello'");
  expect(calculate('1-.')).toBeNaN();
  expect(calculate('*')).toBeNaN();
  expect(calculate('/')).toBeNaN();

  // All together expression
  expect(
    calculate(
      '((3.1 + cos(-4) / 2) * max(-6, 6) ^ sin(6) * 9) / tan(log(8.8 + -2) % 7) + (6 * -1 - min(6, -4.2))'
    )
  ).toEqual(
    eval(
      '((3.1 + Math.cos(-4) / 2) * Math.max(-6, 6) ** Math.sin(6) * 9) / Math.tan(Math.log(8.8 + -2) % 7) + (6 * -1 - Math.min(6, -4.2))'
    )
  );
  expect(
    calculate('((3.1+cos(-4)/2)*max(-6,6)^sin(6)*9)/tan(log(8.8+-2)%7)+(6*-1-min(6,-4.2))')
  ).toEqual(
    eval(
      '((3.1+Math.cos(-4)/2)*Math.max(-6,6)**Math.sin(6)*9)/Math.tan(Math.log(8.8+-2)%7)+(6*-1-Math.min(6,-4.2))'
    )
  );
});

test('calculate() with getRandomMathExpression()', () => {
  for (let i = 0; i < 1000; i++) {
    const expr = getRandomMathExpression(getRandomInt(1, 100));
    expect(calculate(expr)).toEqual(eval(convertMathExpressionToEval(expr)));
  }
});

More here: https://gist.github.com/tkrotoff/b0b1d39da340f5fc6c5e2a79a8b6cec0




Another solution that only supports +, -, *, / without parentheses: https://code.tutsplus.com/tutorials/what-they-didnt-tell-you-about-es5s-array-extras--net-28263 (by Felix Bohm)

function calculate(expression: string) {
  const parts = parse(expression); // Tokenize function to be implemented

  // Build an array with all operations reduced to additions
  const processed = new Array<number>();

  for (let i = 0; i < parts.length; i++) {
    const part = parts[i];
    switch (part) {
      case '+': {
        // Ignore
        break;
      }
      case '-': {
        const rightValue = parts[++i];
        if (typeof rightValue === 'number') {
          processed.push(-1 * rightValue);
        } else {
          processed.push(Number.NaN);
        }
        break;
      }
      case '*': {
        const leftValue = processed.pop();
        const rightValue = parts[++i];
        if (typeof leftValue === 'number' && typeof rightValue === 'number') {
          processed.push(leftValue * rightValue);
        } else {
          processed.push(Number.NaN);
        }
        break;
      }
      case '/': {
        const leftValue = processed.pop();
        const rightValue = parts[++i];
        if (typeof leftValue === 'number' && typeof rightValue === 'number') {
          processed.push(leftValue / rightValue);
        } else {
          processed.push(Number.NaN);
        }
        break;
      }
      default: {
        processed.push(part);
      }
    }
  }

  // Add all numbers and return the result
  return processed.reduce((accumulator, currentValue) => accumulator + currentValue);
}
tanguy_k
  • 11,307
  • 6
  • 54
  • 58
0

Here is an algorithmic solution similar to jMichael's that loops through the expression character by character and progressively tracks left/operator/right. The function accumulates the result after each turn it finds an operator character. This version only supports '+' and '-' operators but is written to be extended with other operators. Note: we set 'currOp' to '+' before looping because we assume the expression starts with a positive float. In fact, overall I'm making the assumption that input is similar to what would come from a calculator.

function calculate(exp) {
  const opMap = {
    '+': (a, b) => { return parseFloat(a) + parseFloat(b) },
    '-': (a, b) => { return parseFloat(a) - parseFloat(b) },
  };
  const opList = Object.keys(opMap);

  let acc = 0;
  let next = '';
  let currOp = '+';

  for (let char of exp) {
    if (opList.includes(char)) {
      acc = opMap[currOp](acc, next);
      currOp = char;
      next = '';
    } else {
      next += char;
    } 
  }

  return currOp === '+' ? acc + parseFloat(next) : acc - parseFloat(next);
}
internetross
  • 143
  • 2
  • 10
0

Based on Aniket Kudale's parse

To add context variables to the expression

function parseExpr(str: string, params: any) {
  const names = Object.keys(params);
  const vals = Object.values(params);
  return Function(...names, `'use strict'; return (${str})`)(...vals);
}

example

> parseExpr('age > 50? x : x/2', {x: 40, age: 46})
20

> parseExpr('age > 50? x : x/2', {x: 40, age: 60})
40

Arne Jenssen
  • 1,235
  • 3
  • 14
  • 22
0

Adding a simple version for +, -, / and *, taking float numbers in consideration. Inspired by @kennebec.

function addbits(s) {
  let total = 0;
  s = s.match(/[+\-\*\/]*(\.\d+|\d+(\.\d+)?)/g) || [];
      
  while (s.length) {
    const nv = s.shift();
    if (nv.startsWith('/')) {
      total /= parseFloat(nv.substring(1));
    } else if (nv.startsWith('*')) {
      total *= parseFloat(nv.substring(1));
    } else {
      total += parseFloat(nv);
    }
  }
  return total;
}

var string = '-2*3.5';
console.log(
  addbits(string)
)
Felipe N Moura
  • 1,367
  • 11
  • 13
0

You can also use the eval function

function parseExp(exp){
eval(`const ans = ${exp}`);
return ans;
}

The eval function just simply converts the string into an executable javascript expression, variables declared can also be used outside the eval string.

This is the official documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval

Alvin CHENG
  • 374
  • 2
  • 16
0

This is not completely mine, but this answer is still worth being here.
it's a synthesis between kennebec's answer
and the one here: Check if string is a mathematical expression in JavaScript

const rgx = /(?:(?:^|[-+_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-]?\d+)?\s*))+$/;

function parse(str)
  {
  if (!rgx.test(str)) return 'invalid entry!'
  return Function(`'use strict'; return (${str})`)()
  }

document.write( "1+2+3", ' = ' , parse("1+2+3"), '<br>');
document.write( "1 + 2 * 3", ' = ' , parse("1 + 2 * 3"), '<br>');

document.write( "'alert(\"hello\")'", ' = ' , parse('alert("hello")'), '<br>');
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
0

My own algorithm to convert String data type to an Integer data type in JavaScript

Solution:

let toInt = (string) => string - [];

Usage

let myString = "1024";

console.log(toInt(myString));
Yerenzter
  • 1
  • 4
  • This doesn't address the question at all. Which is about parsing an expression, not just parsing a number. `"1+1"-[]` is `NaN` not `2`. – chrslg Nov 29 '22 at 00:42
0

Try js-expression-eval

It is javascript based string expression evaluation library.

This library can evaluate a string expression and return the result. It supports basic arithmetic operations and it also supports custom variables and functions.

Example:

const parser = new ExpressionParser('A.B + A.C' ,
 { 
    A: (identifier) => {
    switch (identifier) {
        case 'B':
            return 2;
        case 'C':
            return 3;
        }
    }
});
const result = parser.evaluate();
console.log(result); // 5
Praveen N
  • 137
  • 1
  • 7