Evaluar una cadena como expresión matemática en JavaScript

82

¿Cómo analizo y evalúo una expresión matemática en una cadena (por ejemplo '1+1') sin invocar eval(string)para obtener su valor numérico?

Con ese ejemplo, quiero que la función acepte '1+1'y regrese 2.

whereresrhys
fuente
5
Muy similares, pero probablemente no es lo que estás pidiendo: (Function("return 1+1;"))().
Gumbo

Respuestas:

22

Puedes hacer + o - fácilmente:

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

Las matemáticas más complicadas hacen que eval sea más atractivo y, ciertamente, más simple de escribir.

Kennebec
fuente
2
+1: probablemente un poco más general de lo que hice, pero no funcionará para mi situación, ya que puedo tener algo como 1 + -2, y quiero que la expresión regular excluya también las declaraciones no válidas (creo que la suya permitiría algo como "+ 3 + 4 +")
llega el
He publicado a continuación una respuesta actualizada con una expresión regular más corta y que permite espacios entre operadores
Stefan Gabos
17

Alguien tiene que analizar esa cadena. Si no es el intérprete (a través deeval ), entonces deberá ser usted, escribiendo una rutina de análisis para extraer números, operadores y cualquier otra cosa que desee admitir en una expresión matemática.

Entonces, no, no hay ninguna forma (simple) sin eval. Si le preocupa la seguridad (porque la entrada que está analizando no es de una fuente que controla), tal vez pueda verificar el formato de la entrada (a través de un filtro de expresiones regulares de la lista blanca) antes de pasarla eval.

bdukes
fuente
1
No es la seguridad lo que me molesta (ya tengo una expresión regular para el trabajo), es más la carga en el navegador, ya que tengo que procesar muchas cadenas como esta. ¿Podría un analizador personalizado ser más rápido que eval ()?
llega
11
@wheresrhys: ¿Por qué crees que tu analizador, escrito en JS, será más rápido que el proporcionado por el sistema (optimizado, probablemente escrito en C o C ++)?
Mehrdad Afshari
4
eval es, con mucho, la forma más rápida de hacer esto. Sin embargo, una expresión regular no suele ser suficiente para garantizar la seguridad.
levik
1
@wheresrhys: ¿Por qué tienes muchas cadenas como esta? ¿Están siendo generados por un programa? Si es así, la forma más sencilla es calcular el resultado antes de convertirlos en cadenas. De lo contrario, es el momento de escribir su propio analizador.
Phil H
12

Una alternativa a la excelente respuesta de @kennebec, usando una expresión regular más corta y permitiendo espacios entre operadores

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;
}

Úselo como

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

Actualizar

Aquí hay una versión más optimizada

function addbits(s) {
    return (s.replace(/\s/g, '').match(/[+\-]?([0-9\.]+)/g) || [])
        .reduce(function(sum, value) {
            return parseFloat(sum) + parseFloat(value);
        });
}
Stefan Gabos
fuente
1
Esto es perfecto, siempre que solo necesites sumar y restar. ¡Tan poco código, tanto producto! Tenga la seguridad, se está utilizando para siempre :)
Ultroman the Tacoman
10

He creado BigEval para el mismo propósito.
Al resolver expresiones, funciona exactamente igual que Eval()y admite operadores como%, ^, &, ** (potencia) y! (factorial). También se le permite usar funciones y constantes (o decir variables) dentro de la expresión. La expresión se resuelve en el orden PEMDAS, que es común en los lenguajes de programación, incluido 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

También se puede utilizar esas bibliotecas de números grandes para aritmética en caso de que se trate de números con precisión arbitraria.

Avi
fuente
8

Busqué bibliotecas de JavaScript para evaluar expresiones matemáticas y encontré estos dos candidatos prometedores:

  • Evaluador de expresiones de JavaScript : más pequeño y, con suerte, más ligero. Permite expresiones algebraicas, sustituciones y una serie de funciones.

  • mathjs : también permite números complejos, matrices y unidades. Diseñado para ser utilizado tanto por JavaScript en el navegador como por Node.js.

Itangalo
fuente
Ahora probé el Evaluador de expresiones de JavaScript y parece que funciona. (Mathjs probablemente también es genial, pero parece un poco demasiado grande para mis propósitos y también me gusta la funcionalidad de sustitución en JSEE).
Itangalo
7

Recientemente hice esto en C # (no Eval()para nosotros ...) al evaluar la expresión en notación polaca inversa (eso es lo fácil). La parte difícil es analizar la cadena y convertirla en notación polaca inversa. He utilizado el algoritmo de Estación de clasificación , ya que hay un gran ejemplo de Wikipedia y pseudocódigo. Me pareció realmente sencillo implementar ambos y lo recomendaría si aún no ha encontrado una solución o está buscando alternativas.

RichK
fuente
¿Puede proporcionar algún ejemplo o enlace a Wikipedia?
LetynSOFT
@LetynSOFT El pseudocódigo se puede encontrar aquí
Mayonnaise2124
6

Esta es una pequeña función que reuní hace un momento para resolver este problema: construye la expresión analizando la cadena un carácter a la vez (aunque en realidad es bastante rápido). Esto tomará cualquier expresión matemática (limitada a operadores +, -, *, / solamente) y devolverá el resultado. También puede manejar valores negativos y operaciones de números ilimitados.

Lo único que queda por hacer es asegurarse de que calcula * & / antes de + & -. Agregaré esa funcionalidad más adelante, pero por ahora esto hace lo que necesito ...

/**
* 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
fuente
3

Simple y elegante con Function()

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

parse("1+2+3"); 

Aniket Kudale
fuente
¿Puedes explicar cómo funciona? Soy nuevo en esta sintaxis
pageNotfoUnd
Función ("return (1 + 2 + 3)") (); - es una función anónima. Solo estamos ejecutando el argumento (cuerpo de la función). Función ("{return (1 + 2 + 3)}") ();
Aniket Kudale
ok, ¿cómo se analiza la cadena? & ¿Qué es eso ($ {str}) ) -----() `este paréntesis al fin?
pageNotfoUnd
No veo cómo esto es mejor que eval. Antes de ejecutar este servidor, tenga cuidado parse('process.exit()').
Basti
3

Puede usar un bucle for para verificar si la cadena contiene caracteres no válidos y luego usar un try ... catch con eval para verificar si el cálculo arroja un error como lo eval("2++")haría.

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

o en lugar de una función, puede configurar 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'))

GalaxyCat105
fuente
2

Finalmente, opté por esta solución, que funciona para sumar enteros positivos y negativos (y con una pequeña modificación en la expresión regular también funcionará para decimales):

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;
}

No estoy seguro de si es más rápido que eval (), pero como tengo que realizar la operación muchas veces, me siento mucho más cómodo ejecutando este script que creando un montón de instancias del compilador javascript.

whereresrhys
fuente
1
Aunque returnno se puede usar dentro de una expresión, sum("+1")devuelve NaN .
Gumbo
Siempre evite si el retorno tiene que ir o no dentro de una expresión ternaria. Me gustaría excluir "+1" porque aunque "debería" evaluarse como un número, en realidad no es un ejemplo de una suma matemática en el sentido cotidiano. Mi código está diseñado tanto para evaluar como para filtrar las cadenas permitidas.
Wheresrhys
2

Prueba 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
fuente
2

Creo que parseInty ES6 puede ser útil en esta situación.

==> de esta manera:

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

Lo principal aquí es que parseIntanaliza el número con el operador. El código se puede modificar a las necesidades correspondientes.

fusor
fuente
2

Puede usar esta biblioteca bien mantenida de Github que funciona tanto en Nodejs como en el navegador que es más rápido que otras bibliotecas alternativas proporcionadas aquí

Uso

 mexp = require('math-expression-evaluator')
 var value = mexp.eval(exp);  

Documentación completa

bugwheels94
fuente
2
Revele su afiliación a la biblioteca. Lo mantiene un usuario llamado "bugwheels94", que es su nombre de usuario aquí.
GalaxyCat105
2
Parece que está afiliado al repositorio de GitHub vinculado aquí. Al vincular a algo con lo que está afiliado, debe revelar esa afiliación en su publicación . Sin la divulgación de afiliación, técnicamente se considera spam. Consulte: ¿Qué significa "buena" autopromoción? y el centro de ayuda sobre autopromoción . La divulgación debe ser explícita, pero no es necesario que sea formal. Cuando se trata de algo como esto, la divulgación puede ser algo como "..., un repositorio al que contribuyo", etc. He editado tu publicación para incluir la divulgación.
Makyen
@Makyen Por favor, no impongas tu opinión como reglas. No puedo encontrar en ningún lugar donde esté escrito que un paquete gratuito deba divulgarse en ninguno de los 2 enlaces que ha proporcionado. Lo habría hecho por cortesía, pero ahora, después de una edición innecesaria, soy reacio a hacerlo. Este paquete resolvió el problema del OP y he explicado cómo. El descanso no importa
bugwheels94
1
@ bugwheels94 No importa si el enlace es gratuito o comercial. Si vincula o promociona algo con lo que está afiliado, se requiere divulgación, excepto en algunas circunstancias muy limitadas, que definitivamente no se aplican a esta respuesta. El requisito de divulgar la afiliación ha sido una política durante mucho tiempo. Se ha discutido varias veces en Meta, tanto MSO como MSE. Lamento que no te haya gustado mi edición mínima de tu publicación. Hacer esa edición fue la opción menos invasiva. Lo elegí porque además de no tener divulgación, es una respuesta razonable.
Makyen
1

Pruebe AutoCalculator https://github.com/JavscriptLab/autocalculate Calcular el valor de las entradas y la salida mediante el uso de expresiones de selector

Simplemente agregue un atributo para su entrada de salida como data-ac = "(# firstinput + # secondinput)"

No se necesita ninguna inicialización, solo agregue el atributo data-ac. Descubrirá elementos agregados dinámicamente automáticamente

Para agregar 'Rs' con Salida, simplemente agregue dentro de los corchetes data-ac = "{Rs} (# firstinput + # secondinput)"

Justin jose
fuente
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
Rushikesh Bharad
fuente
1
¿Qué pasa con la resta, la multiplicación y la división? ¿Por qué multiplicar numpor 1?
nathanfranke
Gracias por señalarlo @nathanfranke He actualizado la respuesta para hacerla más genérica. Ahora admite las 4 operaciones. Y multiplicar por 1 era convertirlo de cadena en número. Lo cual podemos lograr haciendo + num también.
Rushikesh Bharad
0

Aquí hay una solución algorítmica similar a la de jMichael que recorre la expresión carácter por carácter y rastrea progresivamente izquierda / operador / derecha. La función acumula el resultado después de cada turno que encuentra un carácter operador. Esta versión solo admite los operadores '+' y '-', pero está escrita para ampliarse con otros operadores. Nota: establecemos 'currOp' en '+' antes del bucle porque asumimos que la expresión comienza con un flotante positivo. De hecho, en general, estoy asumiendo que la entrada es similar a lo que vendría de una calculadora.

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);
}
internetros
fuente