Оценка строки как математического выражения в JavaScript


84

Как мне проанализировать и оценить математическое выражение в строке (например '1+1'), не вызывая eval(string)его числового значения?

В этом примере я хочу, чтобы функция принимала '1+1'и возвращала 2.


5
Очень похоже , но это, вероятно , не то , что вы просите: (Function("return 1+1;"))().
Gumbo

Ответы:



23

Вы можете легко сделать + или -:

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

Более сложная математика делает eval более привлекательным и, безусловно, более простым в написании.


2
+1 - Вероятно, немного более общий, чем то, что я использовал, но это не сработает для моей ситуации, так как у меня может быть что-то вроде 1 + -2, и я хочу, чтобы регулярное выражение также исключало недопустимые утверждения (я думаю, что ваше позволит что-то вроде "+ 3 + 4 +")
где

Ниже я опубликовал обновленный ответ с более коротким регулярным выражением и с учетом пробелов между операторами
Стефан Габос

17

Кто-то должен проанализировать эту строку. Если это не интерпретатор (через eval), то вам нужно будет написать подпрограмму синтаксического анализа для извлечения чисел, операторов и всего остального, что вы хотите поддержать в математическом выражении.

Итак, нет, без (простого) пути нет eval. Если вас беспокоит безопасность (поскольку входные данные, которые вы анализируете, не из контролируемого вами источника), возможно, вы можете проверить формат входных данных (с помощью фильтра регулярных выражений белого списка), прежде чем передавать его eval?


1
Меня беспокоит не безопасность (у меня уже есть регулярное выражение для этой работы), а скорее нагрузка на браузер, так как мне нужно обрабатывать много таких строк. Может ли пользовательский анализатор быть быстрее, чем eval ()?
wheresrhys

11
@whererhys: Почему вы думаете, что ваш парсер, написанный на JS, будет быстрее, чем тот, который предоставляет система (оптимизированный, вероятно, написанный на C или C ++)?
mmx

4
eval - безусловно, самый быстрый способ сделать это. Однако регулярного выражения обычно недостаточно для обеспечения безопасности.
levik

1
@whererhys: Почему у вас много таких строк? Они генерируются программой? В таком случае самый простой способ - вычислить результат до преобразования в строки. В противном случае пришло время написать свой собственный парсер.
Phil H

13

Альтернатива отличному ответу @kennebec с использованием более короткого регулярного выражения и допуском пробелов между операторами

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

Используйте это как

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

Обновить

Вот более оптимизированная версия

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

1
Это идеально, если вам нужно только сложение и вычитание. Так мало кода, столько продукта! Будьте уверены, это используется во благо :)
Ultroman the Tacoman

10

Я создал BigEval для той же цели.
При решении выражений он работает точно так же Eval()и поддерживает такие операторы, как%, ^, &, ** (мощность) и! (факториал). Вы также можете использовать функции и константы (или, скажем, переменные) внутри выражения. Выражение решается в порядке PEMDAS, который является обычным для языков программирования, включая 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

Также можно использовать эти библиотеки Big Number для арифметики, если вы имеете дело с числами с произвольной точностью.


8

Я искал библиотеки JavaScript для вычисления математических выражений и нашел этих двух многообещающих кандидатов:

  • Оценщик выражений JavaScript : меньше по размеру и, надеюсь, легче. Позволяет использовать алгебраические выражения, замены и ряд функций.

  • mathjs : позволяет также использовать комплексные числа, матрицы и единицы измерения. Создан для использования как в браузере JavaScript, так и в Node.js.


Я сейчас протестировал JavaScript Expression Evaluator, и, похоже, он работает. (mathjs, вероятно, тоже крут, но он кажется слишком большим для моих целей, и мне также нравится функциональность подстановки в JSEE.)
Итангало

7

Я недавно сделал это на C # (нет Eval()для нас ...), оценив выражение в обратной польской нотации (это простой бит). Самая сложная часть - это фактически разобрать строку и преобразовать ее в обратную польскую нотацию. Я использовал алгоритм Shunting Yard , так как есть отличный пример в Википедии и псевдокоде. Мне показалось очень простым реализовать оба варианта, и я бы порекомендовал это, если вы еще не нашли решения или ищете альтернативы.


Вы можете привести пример или ссылку на Википедию?
LetynSOFT

@LetynSOFT Псевдокод можно найти здесь
Mayonnaise2124

6

Это небольшая функция, которую я собрал только что для решения этой проблемы - она ​​строит выражение, анализируя строку по одному символу за раз (хотя на самом деле это довольно быстро). Это будет принимать любое математическое выражение (ограниченное только операторами +, -, *, /) и возвращать результат. Он также может обрабатывать отрицательные значения и неограниченное количество операций.

Единственное, что осталось сделать, это убедиться, что он вычисляет * & / перед + & -. Позже добавлю эту функциональность, а пока это то, что мне нужно ...

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

Просто и элегантно с Function()

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

parse("1+2+3"); 


не могли бы вы объяснить, как это работает? Я новичок в этом синтаксисе
pageNotfoUnd

Функция ("return (1 + 2 + 3)") (); - это анонимная функция. Мы просто выполняем аргумент (тело функции). Функция ("{return (1 + 2 + 3)}") ();
Аникет Кудале

хорошо, как строка разбирается? & что это ($ {str}) ) -----() `эта скобка наконец?
pageNotfoUnd

Не понимаю, чем это лучше, чем eval. Прежде чем запускать эту серверную часть, остерегайтесь parse('process.exit()').
Basti

3

Вы можете использовать цикл for, чтобы проверить, содержит ли строка какие-либо недопустимые символы, а затем использовать try ... catch с eval, чтобы проверить, не вызывает ли вычисление подобную ошибку 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'))

или вместо функции вы можете установить 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

В конце концов я выбрал это решение, которое работает для суммирования положительных и отрицательных целых чисел (и с небольшой модификацией регулярного выражения будет работать и для десятичных чисел):

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

Я не уверен, что это быстрее, чем eval (), но, поскольку мне приходится выполнять операцию много раз, мне гораздо удобнее запускать этот скрипт, чем создавать множество экземпляров компилятора javascript


1
Хотя returnне может использоваться внутри выражения, sum("+1")возвращает NaN .
Gumbo

Всегда предугадывайте, должно ли return входить в тройное выражение или нет. Я хотел бы исключить «+1», потому что, хотя он «должен» оцениваться как число, на самом деле это не пример математической суммы в повседневном смысле. Мой код предназначен как для оценки, так и для фильтрации допустимых строк.
wheresrhys 06


2

Я считаю, что parseIntи ES6 может быть полезен в этой ситуации.

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

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

Здесь главное, что номер parseIntразбирается с оператором. Код может быть изменен в соответствии с потребностями.


1

Попробуйте AutoCalculator https://github.com/JavscriptLab/autocalculate Рассчитайте входные значения и выходные данные с помощью выражений селектора

Просто добавьте атрибут для выходного ввода, например data-ac = "(# firstinput + # secondinput)"

Нет необходимости в какой-либо инициализации, просто добавьте только атрибут data-ac. Он автоматически обнаружит динамически добавленные элементы

Для добавления 'Rs' с Output просто добавьте в фигурные скобки 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
А как насчет вычитания, умножения и деления? Зачем умножать numна 1?
nathanfranke

Спасибо, что указали на это @nathanfranke. Я обновил ответ, чтобы сделать его более общим. Теперь он поддерживает все 4 операции. А умножение на 1 было преобразованием из строки в число. Этого мы также можем добиться, выполнив + num.
Рушикеш Бхарад,

0

Вот алгоритмическое решение, подобное jMichael, которое перебирает выражение символ за символом и постепенно отслеживает left / operator / right. Функция накапливает результат после каждого хода нахождения символа оператора. Эта версия поддерживает только операторы «+» и «-», но написана для расширения другими операторами. Примечание: мы устанавливаем currOp на «+» перед циклом, потому что предполагаем, что выражение начинается с положительного числа с плавающей запятой. Фактически, в целом я предполагаю, что ввод аналогичен тому, что поступает с калькулятора.

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);
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.