Оцінка рядка як математичного виразу в JavaScript


84

Як мені проаналізувати та оцінити математичний вираз у рядку (наприклад '1+1'), не викликаючи його, eval(string)щоб отримати його числове значення?

У цьому прикладі я хочу, щоб функція приймала '1+1'і повертала 2.


5
Дуже схоже , але це, ймовірно , не те , що ви просите: (Function("return 1+1;"))().
Гамбо,

Відповіді:



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 +")
Wheresrhys,

Нижче я опублікував оновлену відповідь із більш коротким регулярним виразом та з урахуванням пробілів між операторами
Стефан Габос,

17

Хтось повинен проаналізувати цей рядок. Якщо це не інтерпретатор (через eval), то це повинні бути ви, написавши процедуру аналізу для вилучення чисел, операторів та всього іншого, що ви хочете підтримати в математичному виразі.

Отже, ні, не існує жодного (простого) способу без eval. Якщо ви стурбовані безпекою (оскільки вхід, який ви розбираєте, не надходить із контрольованого вами джерела), можливо, ви можете перевірити формат вводу (за допомогою фільтра регулярних виразів білого списку), перш ніж передати його eval?


1
Мене турбує не безпека (у мене вже є регулярний вираз для роботи), це більше навантаження на браузер, оскільки мені доводиться обробляти багато таких рядків. Чи може користувацький синтаксичний аналізатор бути швидшим за швидкість, ніж eval ()?
Wheresrhys

11
@wheresrhys: Чому ви думаєте, що ваш парсер, написаний на JS, буде швидшим, ніж надана система (оптимізований, можливо, написаний на C або C ++)?
mmx

4
eval - це найшвидший спосіб зробити це. Однак регулярного виразу зазвичай недостатньо для забезпечення безпеки.
levik

1
@wheresrhys: Чому у вас багато таких струн? Вони генеруються програмою? Якщо так, то найпростіший спосіб - це обчислити результат, перш ніж вони будуть перетворені в рядки. В іншому випадку час написання власного парсера.
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

Можна також використовувати ці бібліотеки великих чисел для арифметики, якщо ви маєте справу з числами з довільною точністю.


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

Функція ("повернення (1 + 2 + 3)")) (); - це анонімна функція. Ми просто виконуємо аргумент (тіло функції). Функція ("{return (1 + 2 + 3)}")) ();
Aniket Kudale

добре, як аналізується рядок? & що це ($ {str}) ) -----() `нарешті ця дужка?
pageNotfoUnd

Я не розумію, як це краще, ніж eval. Перш ніж запускати цю сторону сервера, остерігайтеся parse('process.exit()').
Басті,

3

Ви можете використовувати цикл for, щоб перевірити, чи містить рядок якісь неприпустимі символи, а потім скористатися функцією try ... catch with 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 .
Гамбо

Завжди передбачайте, чи має повернення входити всередину трійкового виразу чи ні. Я хотів би виключити "+1", оскільки, хоча його "слід" оцінювати як число, насправді це не приклад математичної суми у повсякденному розумінні. Мій код призначений для оцінки та фільтрації допустимих рядків.
Wheresrhys


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' з виходом, просто додайте всередину фігурної дужки 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, яке перебирає символ виразу за символом і поступово відстежує ліворуч / оператор / праворуч. Функція накопичує результат після кожного повороту, коли вона знаходить символ оператора. Ця версія підтримує лише оператори '+' та '-', але написана для розширення з іншими операторами. Примітка: ми встановлюємо для '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.