Auswerten einer Zeichenfolge als mathematischer Ausdruck in JavaScript

82

Wie analysiere und bewerte ich einen mathematischen Ausdruck in einer Zeichenfolge (z. B. '1+1'), ohne ihn aufzurufen eval(string), um seinen numerischen Wert zu erhalten?

In diesem Beispiel möchte ich, dass die Funktion akzeptiert '1+1'und zurückgibt 2.

whereesrhys
quelle
5
Sehr ähnlich, aber es ist wahrscheinlich nicht das, wonach Sie fragen : (Function("return 1+1;"))().
Gumbo

Antworten:

22

Sie können + oder - einfach tun:

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

Kompliziertere Mathematik macht eval attraktiver - und sicherlich einfacher zu schreiben.

kennebec
quelle
2
+1 - Wahrscheinlich etwas allgemeiner als das, was ich gemacht habe, aber es wird für meine Situation nicht funktionieren, da ich möglicherweise so etwas wie 1 + -2 habe, und ich möchte, dass der reguläre Ausdruck auch ungültige Anweisungen ausschließt (ich denke, Ihre würden dies zulassen so etwas wie "+ 3 + 4 +")
Wheresrhys
Ich habe unten eine aktualisierte Antwort mit einem kürzeren regulären Ausdruck gepostet und Leerzeichen zwischen Operatoren
berücksichtigt
17

Jemand muss diese Zeichenfolge analysieren. Wenn es nicht der Interpreter (via eval) ist, müssen Sie es sein und eine Parsing-Routine schreiben, um Zahlen, Operatoren und alles andere zu extrahieren, das Sie in einem mathematischen Ausdruck unterstützen möchten.

Also, nein, es gibt keinen (einfachen) Weg ohne eval. Wenn Sie sich Sorgen um die Sicherheit machen (weil die Eingabe, die Sie analysieren, nicht von einer Quelle stammt, die Sie steuern), können Sie möglicherweise das Format der Eingabe (über einen Regex-Filter für die Whitelist) überprüfen, bevor Sie sie an übergeben eval?

bdukes
quelle
1
Es ist nicht die Sicherheit, die mich stört (ich habe bereits einen regulären Ausdruck für den Job), sondern eher die Belastung des Browsers, da ich viele solcher Zeichenfolgen verarbeiten muss. Könnte ein benutzerdefinierter Parser möglicherweise schneller als eval () sein?
Wheresrhys
11
@wheresrhys: Warum sollte Ihr in JS geschriebener Parser schneller sein als der vom System bereitgestellte (optimiert, wahrscheinlich in C oder C ++ geschrieben)?
Mehrdad Afshari
4
eval ist bei weitem der schnellste Weg, dies zu tun. Ein regulärer Ausdruck reicht jedoch im Allgemeinen nicht aus, um die Sicherheit zu gewährleisten.
Levik
1
@wheresrhys: Warum hast du so viele Strings? Werden sie von einem Programm generiert? In diesem Fall ist es am einfachsten, das Ergebnis zu berechnen, bevor sie in Zeichenfolgen konvertiert werden. Andernfalls ist es Zeit, Ihren eigenen Parser zu schreiben.
Phil H
12

Eine Alternative zur hervorragenden Antwort von @kennebec, bei der ein kürzerer regulärer Ausdruck verwendet wird und Leerzeichen zwischen Operatoren zugelassen werden

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

Verwenden Sie es wie

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

Aktualisieren

Hier ist eine optimierte Version

function addbits(s) {
    return (s.replace(/\s/g, '').match(/[+\-]?([0-9\.]+)/g) || [])
        .reduce(function(sum, value) {
            return parseFloat(sum) + parseFloat(value);
        });
}
Stefan Gabos
quelle
1
Dies ist perfekt, solange Sie nur Addition und Subtraktion benötigen. So wenig Code, so viel Produkt!
Seien
10

Ich habe BigEval für den gleichen Zweck erstellt.
Beim Lösen von Ausdrücken funktioniert es genauso wie Eval()Operatoren wie%, ^, &, ** (Potenz) und! (Fakultät). Sie können auch Funktionen und Konstanten (oder Variablen) innerhalb des Ausdrucks verwenden. Der Ausdruck wird in der PEMDAS-Reihenfolge gelöst , die in Programmiersprachen einschließlich JavaScript üblich ist.

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

Es kann auch vorgenommen werden, diese Big Number-Bibliotheken für die Arithmetik zu verwenden, falls Sie mit Zahlen mit beliebiger Genauigkeit arbeiten.

Avi
quelle
8

Ich suchte nach JavaScript-Bibliotheken zur Bewertung mathematischer Ausdrücke und fand diese beiden vielversprechenden Kandidaten:

  • JavaScript Expression Evaluator : Kleiner und hoffentlich leichter. Ermöglicht algebraische Ausdrücke, Substitutionen und eine Reihe von Funktionen.

  • mathjs : Ermöglicht auch komplexe Zahlen, Matrizen und Einheiten. Entwickelt, um sowohl von JavaScript im Browser als auch von Node.js verwendet zu werden.

Itangalo
quelle
Ich habe jetzt den JavaScript Expression Evaluator getestet und er scheint zu rocken. (Mathjs rockt wahrscheinlich auch, aber es scheint ein bisschen zu groß für meine Zwecke und ich mag auch die Substitutionsfunktionalität in JSEE.)
Itangalo
7

Ich habe dies kürzlich in C # getan (nein Eval()für uns ...), indem ich den Ausdruck in umgekehrter polnischer Notation ausgewertet habe (das ist das einfache Stück). Der schwierige Teil besteht darin, die Zeichenfolge zu analysieren und in die umgekehrte polnische Notation umzuwandeln. Ich habe den Shunting Yard-Algorithmus verwendet , da es ein großartiges Beispiel für Wikipedia und Pseudocode gibt. Ich fand es sehr einfach, beide zu implementieren, und ich würde dies empfehlen, wenn Sie noch keine Lösung gefunden haben oder nach Alternativen suchen.

RichK
quelle
Können Sie ein Beispiel oder einen Link zur Wikipedia angeben?
LetynSOFT
@ LetynSOFT Den Pseudocode finden Sie hier
Mayonnaise2124
6

Dies ist eine kleine Funktion, die ich gerade zusammengestellt habe, um dieses Problem zu lösen. Sie baut den Ausdruck auf, indem die Zeichenfolge zeichenweise analysiert wird (eigentlich ist sie jedoch ziemlich schnell). Dies nimmt jeden mathematischen Ausdruck (beschränkt auf +, -, *, / Operatoren) und gibt das Ergebnis zurück. Es kann auch negative Werte und unbegrenzte Zahlenoperationen verarbeiten.

Sie müssen nur noch sicherstellen, dass * & / vor + & - berechnet wird. Ich werde diese Funktionalität später hinzufügen, aber im Moment macht das, was ich brauche ...

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

Einfach und elegant mit Function()

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

parse("1+2+3"); 

Aniket Kudale
quelle
Kannst du bitte erklären, wie es funktioniert? Ich bin neu in dieser Syntax
pageNotfoUnd
Funktion ("return (1 + 2 + 3)") (); - Es ist eine anonyme Funktion. Wir führen nur das Argument (Funktionskörper) aus. Funktion ("{return (1 + 2 + 3)}") ();
Aniket Kudale
ok wie wird die Zeichenfolge analysiert? & was ist das ($ {str}) ) -----() `diese Klammer endlich?
pageNotfoUnd
Ich sehe nicht, wie das besser ist als eval. Beachten Sie Folgendes, bevor Sie diese serverseitige Version ausführen parse('process.exit()').
Basti
3

Sie können eine for-Schleife verwenden, um zu überprüfen, ob die Zeichenfolge ungültige Zeichen enthält, und dann einen try ... catch mit eval verwenden, um zu überprüfen, ob die Berechnung einen Fehler wie folgt auslöst eval("2++").

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

oder anstelle einer Funktion können Sie einstellen 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
quelle
2

Ich habe mich schließlich für diese Lösung entschieden, die zum Summieren positiver und negativer Ganzzahlen funktioniert (und mit einer kleinen Änderung des regulären Ausdrucks auch für Dezimalstellen funktioniert):

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

Ich bin mir nicht sicher, ob es schneller als eval () ist, aber da ich die Operation oft ausführen muss, ist es für mich weitaus komfortabler, dieses Skript auszuführen, als viele Instanzen des Javascript-Compilers zu erstellen

whereesrhys
quelle
1
Obwohl returnnicht innerhalb eines Ausdrucks verwendet werden, sum("+1")gibt NaN .
Gumbo
Immer vergessen, ob die Rückkehr in einen ternären Ausdruck gehen muss oder nicht. Ich möchte "+1" ausschließen, denn obwohl es als Zahl ausgewertet werden sollte, ist es nicht wirklich ein Beispiel für eine mathematische Summe im alltäglichen Sinne. Mein Code dient zum Auswerten und Filtern nach zulässigen Zeichenfolgen.
Wheresrhys
2

Versuchen Sie es mit 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
quelle
2

Ich glaube das parseIntund ES6 kann in dieser Situation hilfreich sein

==> auf diese Weise:

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

Die Hauptsache hier ist, dass parseIntdie Nummer mit dem Operator analysiert wird. Der Code kann an die entsprechenden Anforderungen angepasst werden.

Fixiereinheit
quelle
2

Sie können diese gut gepflegte Bibliothek von Github verwenden, die sowohl auf Nodejs als auch auf dem Browser funktioniert und schneller ist als andere hier bereitgestellte alternative Bibliotheken

Verwendung

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

Vollständige Dokumentation

Bugwheels94
quelle
2
Bitte geben Sie Ihre Zugehörigkeit zur Bibliothek bekannt. Es wird von einem Benutzer namens "bugwheels94" gepflegt, der hier Ihr Benutzername ist.
GalaxyCat105
2
Anscheinend sind Sie mit dem hier verlinkten GitHub-Repository verbunden. Wenn Sie auf etwas verlinken, mit dem Sie verbunden sind, müssen Sie diese Zugehörigkeit in Ihrem Beitrag offenlegen . Ohne Offenlegung der Zugehörigkeit wird dies technisch als Spam betrachtet. Bitte sehen Sie: Was bedeutet "gute" Eigenwerbung? und das Hilfezentrum zur Eigenwerbung . Die Offenlegung muss explizit erfolgen, muss jedoch nicht formal sein. Wenn es so etwas ist, kann Offenlegung einfach so etwas wie "..., ein Repository, zu dem ich beitrage" usw. sein. Ich habe Ihren Beitrag so bearbeitet, dass er Offenlegung enthält.
Makyen
@Makyen Bitte setzen Sie Ihre Meinung nicht als Regeln durch. Ich kann nirgendwo finden, wo geschrieben steht, dass ein kostenloses Paket in keinem der 2 von Ihnen angegebenen Links veröffentlicht werden muss. Ich hätte das aus Höflichkeit getan, aber jetzt, nach unnötiger Bearbeitung, zögere ich, das zu tun. Dieses Paket hat das Problem des OP gelöst und ich habe erklärt, wie. Ruhe spielt keine Rolle
bugwheels94
1
@ bugwheels94 Es spielt keine Rolle, ob das, auf das Sie verlinken, kostenlos oder kommerziell ist. Wenn Sie etwas verlinken oder bewerben, mit dem Sie verbunden sind, ist eine Offenlegung erforderlich, außer unter sehr begrenzten Umständen, die für diese Antwort definitiv nicht gelten. Das Erfordernis der Offenlegung der Zugehörigkeit ist seit sehr langer Zeit eine Politik. Es wurde mehrfach über Meta diskutiert, sowohl über MSO als auch über MSE. Es tut mir leid, dass Ihnen meine minimale Bearbeitung Ihres Beitrags nicht gefallen hat. Diese Bearbeitung war die am wenigsten invasive Option. Ich habe es gewählt, weil es eine vernünftige Antwort ist, abgesehen davon, dass es keine Offenlegung gibt.
Makyen
1
Wenn Sie zusätzliche Informationen wünschen, lesen Sie bitte: Was ist die genaue Definition von "Spam" für Stapelüberlauf? und Was macht etwas Spam und die Hilfe zentriert sich auf das Verhalten .
Makyen
1

Versuchen Sie AutoCalculator https://github.com/JavscriptLab/autocalculate Berechnen Sie den Eingabewert und die Ausgabe mithilfe von Auswahlausdrücken

Fügen Sie einfach ein Attribut für Ihre Ausgabeeingabe hinzu, z. B. data-ac = "(# firstinput + # secondinput)"

Keine Initialisierung erforderlich. Fügen Sie nur das Attribut data-ac hinzu. Dynamisch hinzugefügte Elemente werden automatisch ermittelt

Für das Hinzufügen von 'Rs' mit der Ausgabe fügen Sie einfach die geschweifte Klammer hinzu data-ac = "{Rs} (# firstinput + # secondinput)"

Justin Jose
quelle
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
quelle
1
Was ist mit Subtraktion, Multiplikation und Division? Warum nummit 1 multiplizieren ?
Nathanfranke
Vielen Dank für den Hinweis @nathanfranke Ich habe die Antwort aktualisiert, um sie allgemeiner zu gestalten. Jetzt unterstützt es alle 4 Operationen. Und ein Vielfaches von 1 bestand darin, es von einer Zeichenfolge in eine Zahl umzuwandeln. Was wir auch mit + num erreichen können.
Rushikesh Bharad
0

Hier ist eine algorithmische Lösung ähnlich der von jMichael, die den Ausdruck Zeichen für Zeichen durchläuft und nach und nach links / Operator / rechts verfolgt. Die Funktion sammelt das Ergebnis nach jeder Runde, in der sie ein Operatorzeichen findet. Diese Version unterstützt nur die Operatoren '+' und '-', ist jedoch so geschrieben, dass sie mit anderen Operatoren erweitert werden kann. Hinweis: Wir setzen 'currOp' vor dem Looping auf '+', da wir davon ausgehen, dass der Ausdruck mit einem positiven Float beginnt. Insgesamt gehe ich davon aus, dass die Eingabe der eines Taschenrechners ähnelt.

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
quelle