Obliczanie ciągu znaków jako wyrażenia matematycznego w JavaScript

84

Jak przeanalizować i ocenić wyrażenie matematyczne w ciągu (np. '1+1') Bez wywoływania w eval(string)celu uzyskania jego wartości liczbowej?

W tym przykładzie chcę, aby funkcja akceptowała '1+1'i zwracała 2.

wheresrhys
źródło
5
Bardzo podobne, ale to chyba nie pytasz o: (Function("return 1+1;"))().
Gumbo

Odpowiedzi:

73

Możesz użyć biblioteki JavaScript Expression Evaluator , która umożliwia wykonywanie takich czynności jak:

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

Lub matematyka , która pozwala na takie rzeczy, jak:

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

W końcu wybrałem matematykę do jednego z moich projektów.

Rafael Vega
źródło
23

Możesz łatwo + lub -:

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

Bardziej skomplikowana matematyka sprawia, że ​​eval jest bardziej atrakcyjny - i na pewno łatwiejszy do napisania.

kennebec
źródło
2
+1 - Prawdopodobnie trochę bardziej ogólne niż to, z czym poszedłem, ale nie zadziała w mojej sytuacji, ponieważ mogę mieć coś takiego jak 1 + -2 i chcę, aby wyrażenie regularne również wykluczało nieprawidłowe stwierdzenia (myślę, że twoje pozwolą coś jak "+ 3 + 4 +")
wheresrhys
Poniżej zamieściłem zaktualizowaną odpowiedź z krótszym wyrażeniem regularnym i dopuszczającą spacje między operatorami
Stefan Gabos
17

Ktoś musi przeanalizować ten ciąg. Jeśli nie jest to interpreter (via eval), to będziesz musiał to być ty, pisząc procedurę parsowania w celu wyodrębnienia liczb, operatorów i wszystkiego, co chcesz obsługiwać w wyrażeniu matematycznym.

Więc nie, nie ma żadnego (prostego) sposobu bez eval. Jeśli obawiasz się o bezpieczeństwo (ponieważ dane wejściowe, które analizujesz, nie pochodzą ze źródła, które kontrolujesz), może możesz sprawdzić format danych wejściowych (za pomocą filtru regex z białej listy) przed przekazaniem go do eval?

bdukes
źródło
1
Nie przeszkadza mi bezpieczeństwo (mam już wyrażenie regularne do tego zadania), ale raczej obciążenie przeglądarki, ponieważ muszę przetwarzać wiele takich ciągów. Czy niestandardowy parser może być szybszy niż eval ()?
wheresrhys
11
@wheresrhys: Dlaczego myślisz, że twój parser, napisany w JS, będzie szybszy niż dostarczony przez system (zoptymalizowany, prawdopodobnie napisany w C lub C ++)?
mmx
4
eval to zdecydowanie najszybszy sposób na zrobienie tego. Jednak wyrażenie regularne zazwyczaj nie wystarcza do zapewnienia bezpieczeństwa.
levik
1
@wheresrhys: Dlaczego masz dużo takich stringów? Czy są generowane przez program? Jeśli tak, najprostszym sposobem jest obliczenie wyniku przed ich konwersją na łańcuchy. W przeciwnym razie jest to czas zapisu własnego parsera.
Phil H
13

Alternatywa dla doskonałej odpowiedzi @kennebec, wykorzystująca krótsze wyrażenie regularne i dopuszczająca spacje między operatorami

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

Użyj tego jak

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

Aktualizacja

Oto bardziej zoptymalizowana wersja

function addbits(s) {
    return (s.replace(/\s/g, '').match(/[+\-]?([0-9\.]+)/g) || [])
        .reduce(function(sum, value) {
            return parseFloat(sum) + parseFloat(value);
        });
}
Stefan Gabos
źródło
1
Jest to idealne rozwiązanie, o ile potrzebujesz tylko dodawania i odejmowania. Tak mało kodu, tyle produktu! Zapewniamy, że jest używany na dobre :)
Ultroman the Tacoman
10

Stworzyłem BigEval w tym samym celu.
W rozwiązywaniu wyrażeń działa dokładnie tak samo, jak Eval()i obsługuje operatory takie jak%, ^, &, ** (potęga) i! (Factorial). Możesz także używać funkcji i stałych (lub powiedzmy zmiennych) wewnątrz wyrażenia. Wyrażenie jest rozwiązywane w kolejności PEMDAS, która jest powszechna w językach programowania, w tym 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

Można również wykorzystać te biblioteki Big Number do arytmetyki, jeśli masz do czynienia z liczbami z dowolną dokładnością.

Avi
źródło
8

Poszukałem bibliotek JavaScript do oceny wyrażeń matematycznych i znalazłem tych dwóch obiecujących kandydatów:

  • JavaScript Expression Evaluator : Mniejszy i, miejmy nadzieję, lżejszy. Umożliwia wyrażenia algebraiczne, podstawienia i szereg funkcji.

  • mathjs : umożliwia również liczby zespolone, macierze i jednostki. Zbudowany do użytku zarówno przez JavaScript w przeglądarce, jak i Node.js.

Itangalo
źródło
Przetestowałem teraz JavaScript Expression Evaluator i wydaje się, że działa. (mathjs prawdopodobnie też rządzi, ale wydaje mi się, że jest trochę za duży dla moich celów, a także podoba mi się funkcja zastępowania w JSEE.)
Itangalo,
7

Niedawno zrobiłem to w C # (nie Eval()dla nas ...), oceniając wyrażenie w odwrotnej notacji polskiej (to łatwy kawałek). Najtrudniejsza część polega na parsowaniu struny i zamianie jej na odwrotną notację polską. Użyłem algorytmu manewrowania , ponieważ jest świetny przykład na Wikipedii i pseudokodzie. Zauważyłem, że wdrożenie obu jest naprawdę proste i polecam to, jeśli nie znalazłeś jeszcze rozwiązania lub szukasz alternatyw.

RichK
źródło
Czy możesz podać przykład lub link do Wikipedii?
LetynSOFT
@LetynSOFT Pseudokod można znaleźć tutaj
Mayonnaise2124
6

To jest mała funkcja, którą właśnie teraz dodałem, aby rozwiązać ten problem - buduje wyrażenie, analizując ciąg znaków po jednym znaku na raz (chociaż jest to dość szybkie). Spowoduje to pobranie dowolnego wyrażenia matematycznego (ograniczone tylko do operatorów +, -, *, /) i zwrócenie wyniku. Obsługuje również wartości ujemne i nieograniczoną liczbę operacji.

Jedyne „do zrobienia” to upewnienie się, że oblicza * & / przed + & -. Dodam tę funkcjonalność później, ale na razie robi to, czego potrzebuję ...

/**
* 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
źródło
4

Prosty i elegancki z Function()

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

parse("1+2+3"); 

Aniket Kudale
źródło
czy możesz wyjaśnić, jak to działa? Jestem nowy w tej składni
pageNotfoUnd
Funkcja ("return (1 + 2 + 3)") (); - jego funkcja anonimowa. Po prostu wykonujemy argument (treść funkcji). Funkcja ("{return (1 + 2 + 3)}") ();
Aniket Kudale
ok, jak przetwarzany jest ciąg? i co to jest ($ {str}) ) -----() `ten nawias w końcu?
pageNotfoUnd
Nie widzę, żeby to było lepsze niż eval. Uważaj, zanim uruchomisz tę stronę serwera parse('process.exit()').
Basti
3

Możesz użyć pętli for, aby sprawdzić, czy ciąg zawiera jakiekolwiek nieprawidłowe znaki, a następnie użyć try ... catch z eval, aby sprawdzić, czy obliczenia generują błąd, taki jak eval("2++")by.

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

lub zamiast funkcji możesz ustawić 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
źródło
2

Ostatecznie zdecydowałem się na to rozwiązanie, które działa na sumowanie dodatnich i ujemnych liczb całkowitych (i po niewielkiej modyfikacji wyrażenia regularnego będzie działać również dla liczb dziesiętnych):

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

Nie jestem pewien, czy jest szybszy niż eval (), ale ponieważ muszę wykonywać tę operację wiele razy, jestem o wiele wygodniejszy z uruchamianiem tego skryptu niż tworzenie wielu instancji kompilatora javascript

wheresrhys
źródło
1
Chociaż returnnie można go użyć wewnątrz wyrażenia, sum("+1")zwraca NaN .
Gumbo
Zawsze przewiduj, czy powrót musi, czy nie może wejść w potrójne wyrażenie. Chciałbym wykluczyć „+1”, ponieważ chociaż „powinno” być obliczane jako liczba, tak naprawdę nie jest przykładem sumy matematycznej w codziennym sensie. Mój kod jest przeznaczony do oceny i filtrowania dozwolonych ciągów.
wheresrhys
2

Spróbuj 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
źródło
2

Wierzę, że parseInti ES6 mogą być pomocne w tej sytuacji

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

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

Najważniejsze jest to, że parseIntanalizuje liczbę z operatorem. Kod można modyfikować do odpowiednich potrzeb.

utrwalacz
źródło
1

Wypróbuj AutoCalculator https://github.com/JavscriptLab/autocalculate Oblicz wartość wejściową i wyjściową za pomocą wyrażeń selektora

Po prostu dodaj atrybut do danych wyjściowych, taki jak data-ac = "(# firstinput + # secondinput)"

Nie ma potrzeby żadnej inicjalizacji, wystarczy dodać tylko atrybut data-ac. Automatycznie wykryje dynamicznie dodane elementy

W przypadku dodania „Rs” z wyjściem po prostu dodaj w nawiasach klamrowych data-ac = "{Rs} (# firstinput + # secondinput)"

Justin Jose
źródło
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
źródło
1
A co z odejmowaniem, mnożeniem i dzieleniem? Po co mnożyć numprzez 1?
nathanfranke
Dziękuję za wskazanie tego @nathanfranke Zaktualizowałem odpowiedź, aby była bardziej ogólna. Teraz obsługuje wszystkie 4 operacje. A wielokrotność przez 1 polegała na zamianie go z ciągu na liczbę. Co możemy osiągnąć również wykonując + num.
Rushikesh Bharad
0

Oto rozwiązanie algorytmiczne podobne do rozwiązania jMichaela, które przechodzi przez wyrażenie znak po znaku i stopniowo śledzi lewo / operator / prawo. Funkcja kumuluje wynik po każdej turze, w której znajduje znak operatora. Ta wersja obsługuje tylko operatory „+” i „-”, ale została napisana w celu rozszerzenia o inne operatory. Uwaga: przed zapętleniem ustawiamy „currOp” na „+”, ponieważ zakładamy, że wyrażenie zaczyna się od dodatniego pływaka. W rzeczywistości ogólnie zakładam, że dane wejściowe są podobne do tego, co pochodzi z kalkulatora.

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
źródło