Đánh giá một chuỗi dưới dạng biểu thức toán học trong JavaScript


82

Làm cách nào để phân tích cú pháp và đánh giá một biểu thức toán học trong một chuỗi (ví dụ '1+1') mà không cần gọi eval(string)ra giá trị số của nó?

Với ví dụ đó, tôi muốn hàm chấp nhận '1+1'và trả về 2.


5
Rất giống nhau nhưng có lẽ không phải những gì bạn đang yêu cầu cho: (Function("return 1+1;"))().
Gumbo

Câu trả lời:



22

Bạn có thể làm + hoặc - dễ dàng:

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

Toán học phức tạp hơn làm cho eval hấp dẫn hơn- và chắc chắn viết đơn giản hơn.


2
+1 - Có thể tổng quát hơn một chút so với những gì tôi đã làm, nhưng nó sẽ không phù hợp với tình huống của tôi vì tôi có thể có một cái gì đó giống như 1 + -2 và tôi muốn regex cũng loại trừ các tuyên bố không hợp lệ (tôi nghĩ rằng của bạn sẽ cho phép một cái gì đó như "+ 3 + 4 +")
wheresrhys

Tôi đã đăng dưới đây một câu trả lời cập nhật với một biểu thức chính quy ngắn hơn và cho phép khoảng trống giữa các nhà khai thác
Stefan Gabos

17

Ai đó phải phân tích cú pháp chuỗi đó. Nếu đó không phải là trình thông dịch (thông qua eval) thì đó sẽ cần là bạn, viết quy trình phân tích cú pháp để trích xuất các số, toán tử và bất kỳ thứ gì khác mà bạn muốn hỗ trợ trong một biểu thức toán học.

Vì vậy, không, không có cách nào (đơn giản) mà không có eval. Nếu bạn lo lắng về bảo mật (vì đầu vào bạn đang phân tích cú pháp không phải từ nguồn bạn kiểm soát), có thể bạn có thể kiểm tra định dạng của đầu vào (thông qua bộ lọc regex danh sách trắng) trước khi chuyển nó đến eval?


1
Không phải vấn đề bảo mật làm phiền tôi (tôi đã có regexp cho công việc), mà là tải nhiều hơn trên trình duyệt vì tôi phải xử lý rất nhiều chuỗi như thế này. Trình phân tích cú pháp tùy chỉnh có thể nhanh hơn eval () không?
wheresrhys

11
@wheresrhys: Tại sao bạn lại nghĩ rằng trình phân tích cú pháp của bạn, được viết bằng JS, sẽ nhanh hơn trình phân tích cú pháp mà hệ thống cung cấp (được tối ưu hóa, có thể được viết bằng C hoặc C ++)?
Mehrdad Afshari

4
eval là cách nhanh nhất để làm điều này. Tuy nhiên, regexp thường không đủ để đảm bảo an ninh.
levik

1
@wheresrhys: Sao bạn nhiều dây thế này? Chúng được tạo ra bởi một chương trình? Nếu vậy, cách đơn giản nhất là tính toán kết quả trước khi chúng được chuyển thành chuỗi. Nếu không, đó là thời gian viết-của-bạn-phân tích cú pháp.
Phil H

12

Một giải pháp thay thế cho câu trả lời tuyệt vời của @kennebec, sử dụng biểu thức chính quy ngắn hơn và cho phép khoảng cách giữa các toán tử

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

Sử dụng nó như

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

Cập nhật

Đây là một phiên bản được tối ưu hóa hơn

function addbits(s) {
    return (s.replace(/\s/g, '').match(/[+\-]?([0-9\.]+)/g) || [])
        .reduce(function(sum, value) {
            return parseFloat(sum) + parseFloat(value);
        });
}

1
Điều này là hoàn hảo, miễn là bạn chỉ cần cộng và trừ. Quá ít mã, quá nhiều sản phẩm! Hãy yên tâm, nó đang được sử dụng cho tốt :)
Ultroman các Tacoman

10

Tôi đã tạo ra BigEval cho cùng một mục đích.
Trong việc giải các biểu thức, nó hoạt động chính xác giống như Eval()và hỗ trợ các toán tử như%, ^, &, ** (power) và! (yếu tố). Bạn cũng được phép sử dụng các hàm và hằng số (hoặc biến) bên trong biểu thức. Biểu thức được giải theo thứ tự PEMDAS phổ biến trong các ngôn ngữ lập trình bao gồm 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

Nó cũng có thể được thực hiện để sử dụng các thư viện Big Number đó cho số học trong trường hợp bạn đang xử lý các số với độ chính xác tùy ý.


8

Tôi đã tìm kiếm các thư viện JavaScript để đánh giá các biểu thức toán học và tìm thấy hai ứng cử viên đầy hứa hẹn sau:

  • Trình đánh giá biểu thức JavaScript : Nhỏ hơn và hy vọng có trọng lượng nhẹ hơn. Cho phép biểu thức đại số, thay thế và một số hàm.

  • mathjs : Cho phép cả số phức, ma trận và đơn vị. Được xây dựng để sử dụng bởi cả JavaScript trong trình duyệt và Node.js.


Bây giờ tôi đã thử nghiệm Trình đánh giá biểu thức JavaScript, và nó có vẻ khá ổn. (mathjs lẽ đá quá, nhưng có vẻ như một chút quá lớn cho các mục đích của tôi và tôi cũng như các chức năng thay thế trong JSEE.)
Itangalo

7

Gần đây tôi đã thực hiện điều này trong C # (không Eval()cho chúng tôi ...) bằng cách đánh giá biểu thức trong Ký hiệu Ba Lan ngược (đó là một chút dễ dàng). Phần khó thực sự là phân tích cú pháp chuỗi và biến nó thành Ký hiệu đánh bóng ngược. Tôi đã sử dụng thuật toán Shunting Yard , vì có một ví dụ tuyệt vời trên Wikipedia và mã giả. Tôi thấy thực sự đơn giản để thực hiện cả hai và tôi khuyên bạn nên làm điều này nếu bạn chưa tìm thấy giải pháp hoặc đang tìm kiếm các giải pháp thay thế.


Bạn có thể cung cấp một số ví dụ hoặc liên kết đến Wikipedia không?
LetynSOFT

@LetynSOFT Có thể tìm thấy mã giả ở đây
Mayonnaise2124

6

Đây là một hàm nhỏ mà tôi đã tổng hợp lại để giải quyết vấn đề này - nó xây dựng biểu thức bằng cách phân tích chuỗi một ký tự tại một thời điểm (mặc dù nó thực sự khá nhanh). Điều này sẽ nhận bất kỳ biểu thức toán học nào (chỉ giới hạn ở các toán tử +, -, *, /) và trả về kết quả. Nó cũng có thể xử lý các giá trị âm và các phép toán số lượng không giới hạn.

Việc "cần làm" duy nhất còn lại là đảm bảo nó tính toán * & / trước + & -. Sẽ thêm chức năng đó sau, nhưng hiện tại điều này không làm những gì tôi cần ...

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

3

Đơn giản và thanh lịch với Function()

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

parse("1+2+3"); 


bạn có thể vui lòng giải thích nó hoạt động như thế nào không? Tôi mới sử dụng cú pháp này
pageNotfoVà

Hàm ("return (1 + 2 + 3)") (); - nó là một chức năng ẩn danh. Chúng tôi chỉ đang thực thi đối số (nội dung hàm). Hàm ("{return (1 + 2 + 3)}") ();
Aniket Kudale

ok làm thế nào chuỗi được phân tích cú pháp? & Cái gì đó ($ {str}) ) -----() `dấu ngoặc này cuối cùng là gì?
pageNotfoUnd

Tôi không thấy cách này tốt hơn so với eval. Trước khi bạn chạy phía máy chủ này, hãy cẩn thận parse('process.exit()').
Basti

3

Bạn có thể sử dụng vòng lặp for để kiểm tra xem chuỗi có chứa bất kỳ ký tự không hợp lệ nào không và sau đó sử dụng try ... catch với eval để kiểm tra xem phép tính có gặp lỗi như thế eval("2++")không.

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

hoặc thay vì một hàm, bạn có thể đặt 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

Cuối cùng, tôi đã tìm ra giải pháp này, giải pháp này hoạt động để tính tổng các số nguyên dương và âm (và với một chút sửa đổi đối với regex cũng sẽ hoạt động cho các số thập phân):

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

Tôi không chắc liệu nó có nhanh hơn eval () hay không, nhưng vì tôi phải thực hiện thao tác này rất nhiều lần nên tôi cảm thấy thoải mái hơn khi chạy tập lệnh này hơn là tạo vô số phiên bản của trình biên dịch javascript


1
Mặc dù returnkhông thể được sử dụng bên trong một biểu thức, nhưng sum("+1")trả về NaN .
Gumbo

Luôn biết trước việc trả về phải hay không thể đi vào bên trong một biểu thức bậc ba. Tôi muốn loại trừ "+1" bởi vì mặc dù nó 'nên' được đánh giá là một số, nó không thực sự là một ví dụ về tổng toán học theo nghĩa hàng ngày. Mã của tôi được thiết kế để đánh giá và lọc các chuỗi được phép.
wheresrhys

2

Hãy thử 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

Tôi tin rằng điều đó parseIntES6 có thể hữu ích trong tình huống này

==> theo cách này:

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

Điều chính ở đây là parseIntphân tích cú pháp số với toán tử. Mã có thể được sửa đổi theo nhu cầu tương ứng.


2

Bạn có thể sử dụng thư viện được duy trì tốt này từ Github , hoạt động trên cả Nodejs và Trình duyệt , nhanh hơn các thư viện thay thế khác được cung cấp tại đây

Sử dụng

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

Tài liệu đầy đủ


2
Vui lòng tiết lộ mối quan hệ của bạn với thư viện. Nó được duy trì bởi một người dùng có tên "bugwheels94", đó là tên người dùng của bạn ở đây.
GalaxyCat105

2
Có vẻ như bạn đang liên kết với kho lưu trữ GitHub được liên kết ở đây. Khi liên kết với một cái gì đó mà bạn liên kết, bạn phải tiết lộ mối liên kết đó trong bài đăng của mình . Nếu không có tiết lộ liên kết, về mặt kỹ thuật, nó được coi là thư rác. Mời bạn xem: Điều gì biểu hiện sự tự quảng cáo "Tốt"? trung tâm trợ giúp về tự quảng cáo . Việc tiết lộ phải rõ ràng, nhưng không cần phải chính thức. Khi nó giống như thế này, tiết lộ có thể chỉ là một cái gì đó như "…, một kho lưu trữ mà tôi đóng góp vào", v.v. Tôi đã chỉnh sửa bài đăng của bạn để bao gồm tiết lộ.
Makyen

@Makyen Vui lòng không áp đặt ý kiến ​​của bạn như quy tắc. Tôi không thể tìm thấy bất cứ nơi nào viết rằng một gói miễn phí phải được tiết lộ trong không có liên kết nào trong số 2 liên kết bạn đã đưa ra. Tôi đã làm điều đó một cách lịch sự nhưng bây giờ sau khi chỉnh sửa không cần thiết, tôi miễn cưỡng làm điều đó. Gói này đã giải quyết vấn đề của OP và tôi đã giải thích cách làm. Nghỉ ngơi không quan trọng
bugwheels94

1
@ bugwheels94 Không quan trọng những gì bạn liên kết đến là miễn phí hay thương mại. Nếu bạn liên kết hoặc quảng bá thứ gì đó mà bạn liên kết, thì việc tiết lộ là bắt buộc, ngoại trừ trong một số trường hợp rất hạn chế, điều này chắc chắn không áp dụng cho câu trả lời này. Yêu cầu tiết lộ liên kết đã là chính sách trong một thời gian rất dài. Nó đã được thảo luận nhiều lần trên Meta, cả MSO và MSE. Tôi xin lỗi bạn không thích chỉnh sửa tối thiểu của tôi về bài đăng của bạn. Thực hiện chỉnh sửa đó là tùy chọn ít xâm lấn nhất. Tôi chọn nó vì ngoài việc không tiết lộ thông tin, đó là một câu trả lời hợp lý.
Makyen


1

Thử AutoCalculator https://github.com/JavscriptLab/autocalculate Tính giá trị đầu vào và đầu ra bằng cách sử dụng biểu thức bộ chọn

Chỉ cần thêm một thuộc tính cho đầu vào đầu ra của bạn như data-ac = "(# firstinput + # secondinput)"

Không cần bất kỳ khởi tạo nào, chỉ cần thêm thuộc tính data-ac. Nó sẽ tự động tìm ra các phần tử được thêm động

FOr thêm 'Rs' với Đầu ra chỉ cần thêm vào bên trong dấu ngoặc nhọn 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
Còn phép trừ, phép nhân và phép chia? Tại sao lại nhân numvới 1?
nathanfranke

Cảm ơn bạn đã chỉ ra @nathanfranke Tôi đã cập nhật câu trả lời để làm cho nó chung chung hơn. Bây giờ nó hỗ trợ tất cả 4 hoạt động. Và bội số 1 là để chuyển đổi nó từ chuỗi thành số. Điều mà chúng ta có thể đạt được bằng cách thực hiện + num.
Rushikesh Bharad

0

Đây là một giải pháp thuật toán tương tự như của jMichael lặp lại thông qua ký tự biểu thức theo ký tự và theo dõi dần dần sang trái / toán tử / phải. Hàm tích lũy kết quả sau mỗi lượt nó tìm thấy một ký tự toán tử. Phiên bản này chỉ hỗ trợ các toán tử '+' và '-' nhưng được viết để mở rộng với các toán tử khác. Lưu ý: chúng tôi đặt 'currOp' thành '+' trước khi lặp vì chúng tôi giả sử biểu thức bắt đầu bằng một số thực dương. Trên thực tế, về tổng thể, tôi đang đưa ra giả định rằng đầu vào tương tự như đầu vào từ máy tính.

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);
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.