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.


5
Bardzo podobne, ale to chyba nie pytasz o: (Function("return 1+1;"))().
— Gumbo

Odpowiedzi:



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.


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?


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

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ą.


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.


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.


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

4

Prosty i elegancki z Function()

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

parse("1+2+3"); 


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


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


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>


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.


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


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
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);
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.