РЕШЕНО: Как реализовать сборку в функции .eval () с рекурсивной функцией

Привет, кодеры,

У меня есть строка «1 + 1» со встроенным javascript в функции eval (), я могу выполнить eval («1 + 1»), поэтому возвращаемое значение будет 2

Но как насчет того, чтобы реализовать эту концепцию в рекурсивной функции в javascript?

function evaluate(str) {

}

evaluate("1+1");
evaluate("1-1");
evaluate("1*1");
evaluate("1/1");

Я пробовал

function evaluate(str) {
  if (str.length === 0) {
    return "";
  }else{
    let angka;
    let symbol;
    for (let i = 0; i < str.length; i++) {
      if (isNaN(str[i])) {
        symbol = str[i];
        break;
      }else{
        angka = str[i]; 
      }
    }
    switch (symbol) {
      case "+": 
        return angka + evaluate(str.slice(1));
      case "-":
          return angka - evaluate(str.slice(1));
      case "/":
        return angka / evaluate(str.slice(1));
      case "*":
        return angka * evaluate(str.slice(1));
      default:
        return parseInt(str[0]) + evaluate(str.slice(1));
    }
  }
}

function evaluate(str) { 
  if (str.length === 0) {
    return ""
  }

  let numbers = "";
  let operator = "";
  let lastIndex = 0;
  for (let i = 0; i <= str.length; i++) {
        if (!isNaN(parseInt(str[i]))) {
          numbers += parseInt(str[i]);          
        }else{
          operator = str[i];
          lastIndex = i;
          break;
        }
  }

  // console.log(numbers, " " , operator , " " , lastIndex);
  lastIndex  = lastIndex < 1 ? 1 : lastIndex;
  if (operator === "+") {
    return numbers + evaluate(str.slice(lastIndex));
  }
}

function evaluate(str) {
  if (str.length === 0) {
    return 1;
  }else{
    let numbers = "";
    for (let i = 0; i <= str.length; i++) {
      if(parseInt(str[i]) >= 0){
        numbers = numbers + "+" +  str[i];
      }else{
        let lengthNumbers = numbers.length > 1 ? numbers.length : 1;
        let tempNumbers = numbers;
        numbers = "";
        return tempNumbers + evaluate(str.slice(lengthNumbers))
      }
    }
  }
}

===========

ОБНОВЛЕНО

Какой я нуб :), а пока это мой ответ (согласно приведенным ниже решениям), спасибо всем

function evaluate(str) {
 if(str.match(/[*/+-]/)){
   let numbers = "";
   for (let i = 0; i < str.length; i++) {
     switch (str[i]) {
       case "+":
        return parseInt(numbers) + evaluate(str.slice(numbers.length+1))     
      case "*":
          return parseInt(numbers) * evaluate(str.slice(numbers.length+1))       
      case "/":
          return parseInt(numbers) / evaluate(str.slice(numbers.length+1))       
      case "-":
          return parseInt(numbers) - evaluate(str.slice(numbers.length+1))       
      default:
        numbers += str[i];
        break;
     }     
   }
 }else{
   return parseInt(str[0]);
 }

}
console.log(evaluate('1+2+3+4+5')) // 15
console.log(evaluate('1*2*3*4*5')) // 120
console.log(evaluate('20/4')) // 5
console.log(evaluate('20-6')) // 14

И никто не работает! Я знаю, что eval спасет мне день, но в этом случае мне нужно решить этот случай, заранее спасибо.


person Onesinus Saut    schedule 13.12.2019    source источник
comment
Несколько вопросов: нужно ли использовать рекурсию? Всегда ли входные данные будут такими же простыми, как ваши примеры, т.е. номер операции с числами, ничего более сложного? Вам нужно, чтобы ваши номера были многозначными?   -  person sp00m    schedule 13.12.2019
comment
Привет, спасибо за ваше время, в тестовом случае возможно многозначное   -  person Onesinus Saut    schedule 14.12.2019


Ответы (3)


Попробуйте этот код

function evaluate(str) {
  var reg = /[*/+-]/
  if(str.match(reg)){
    var temp = ''
    for(let i = 0; i < str.length; i++){
      if(str[i] === '+') {
        return parseInt(temp) + evaluate(str.substring(i+1))
      }
      else if(str[i] === '-') {
        return parseInt(temp) - evaluate(str.substring(i+1))
      }
      else if(str[i] === '*') {
        return parseInt(temp) * evaluate(str.substring(i+1))
      }
      else if(str[i] === '/') {
        return parseInt(temp) / evaluate(str.substring(i+1))
      }
      else {
        temp += str[i]
      }
    }
  }
  else {
    return parseInt(str)
  }
}

console.log(evaluate('1+2+3+4+5')) // 15
console.log(evaluate('1*2*3*4*5')) // 120
console.log(evaluate('20/4')) // 5
console.log(evaluate('20-6')) // 14
person Kode Kite    schedule 14.12.2019

Другой подход - превратить вашу строку в массив, который можно оценить как стек, а затем выполнить простую оценку этого стека. Например, мы можем превратить "10 - 20 + 30 * 2 / 10" в [10, 20, "-", 30, "+", 2, "*", 10, "/"], а затем оценить его, последовательно заменив два верхних элемента стека значением текущей операции, примененной к ним.

Этот метод предназначен только для операций слева направо. Он игнорирует приоритет операторов и не может обрабатывать круглые скобки или небинарные операции. Но этого может хватить для ваших нужд.

Вот реализация:

const evalExpr = (ops) => (expr) => expr
  .replace (/([-+*\/])(\s)*(\d+)/g, (_, a, b, c) => c + b + a)
  .split (/\s+/)                                             
  .map (n => Number(n) || n)
  .reduce (
    (stack, symbol, _, __, op = ops[symbol]) => op           
      ? [... stack.slice(0, -2), op(...stack.slice(-2))] 
      : [... stack, symbol]
    , []
  ) [0];
  
const ops = {
  '+': (a, b) => a + b,
  '-': (a, b) => a - b,
  '*': (a, b) => a * b,
  '/': (a, b) => a / b,
};

const evalNumericExpr = evalExpr (ops);

//  Test
[
  "1 + 1", 
  "1 - 1", 
  "1 * 1", 
  "1 / 1", 
  "2 + 4 + 7", 
  "5 - 7", 
  "5 * 2 + 10",
  "10 - 20 + 30 * 2 / 10",  
  "1 + 5 * 2 + 12 * 2 * 2",
  "10 + 13 - 5 * 3 + 12 / 3 + 3"
] 
.forEach (expr => console .log (`${expr} ==> ${evalNumericExpr (expr)}`))

Шаги replace, split и map вместе превращают эту строку в стек, готовый к обработке. Шаг reduce фактически обрабатывает этот массив, добавляя и удаляя элементы в стеке. Когда "10 - 20 + 30 * 2 / 10" станет [10, 20, "-", 30, "+", 2, "*", 10, "/"], сокращение будет происходить следующим образом:

stack: [],        next: 10   // Push 10 onto the stack
stack: [10],      next: 20   // Push 20 onto the stack
stack: [10, 20],  next: '-'  // Pop 10 and 20 from the stack.  Push (10 - 20) to it
stack: [-10],     next: 30   // Push 30 to the stack
stack: [-10, 30], next: '+'  // Pop -10 and 30 from the stack. Push (-10 + 30) to it
stack: [20],      next: 2    // Push 2 to the stack
stack: [20, 2],   next: '*'  // Pop 20 and 2 from the stack.   Push (20 * 2) to it
stack: [40],      next: 10   // Push 10 to the stack
stack: [40, 10],  next: '/'  // Pop 40 and 10 from the stack.  Push (40 / 10) to it
stack: [4]                   // For a well-formed expression, the stack now has just
                             // one element on it, and that's your result.

Есть много способов расширить это. Очевидно, что добавлять новые бинарные операции тривиально. Мы также могли бы добавить к сокращению другие операции с арностью (хотя было бы сложнее преобразовать строку в формат стека), заменив -2 в сокращении на -op.length. Если бы мы хотели обрабатывать десятичные числа, мы могли бы просто изменить регулярное выражение на что-то вроде /([-+*\/])(\s)*(\-?\d+(:?\.\d+)?)/g.

И поздравляю нас. Мы только что написали начало интерпретатора Forth!


Обновлять

В частности, был задан вопрос о том, как это сделать рекурсивно, и я написал рекурсивную версию, которая в целом была проще, чем приведенная выше. Но потом я понял, как его можно легко расширить для обработки круглых скобок и соблюдения приоритета операторов. Это уже не обязательно проще, но это интересный подход, который мы могли бы легко расширить с помощью других операторов и различий уровней приоритета:

// Does not test for well-formedness.  Will likely return NaN for
// ill-formed expression strings
const evalNumericExpr = (
  expr, 
  [regex, fn, ops] = [
    // parentheses
    [/\(([^())]*)\)/, (ops, expr, regex) => evalNumericExpr (expr.replace(regex, (_, e) => evalNumericExpr(e))), {}],
    // multiplication and division
    [/\s*(\-?\d+)\s*([/*])\s*(\-?\d+)/, (ops, expr, regex) => evalNumericExpr (expr .replace (
      regex,
      (_, a, op, b) => String(ops[op](Number(a),  Number(b)))
    )), {'*': (a, b) => a * b, '/': (a, b) => a / b}],
    // addition and subtraction
    [/\s*(\-?\d+)\s*([+-])\s*(\-?\d+)/, (ops, expr, regex) => evalNumericExpr (expr .replace (
      regex,
      (_, a, op, b) => String(ops[op](Number(a),  Number(b)))
    )), {'+': (a, b) => a + b, '-': (a, b) => a - b}],
    // everything else
    [/.?/, (ops, expr, regex) => Number(expr.trim()), {}]
  ].find(([regex, fn, ops]) => regex.test(expr))
) => fn(ops, expr, regex)


//  Test
; [
  "1 + 5", 
  "7 - 2", 
  "3 * 5", 
  "21 / 3", 
  "2 + 4 + 7", 
  "5 - 7", 
  "5 * 2 + 10",
  "5 * 2 + (3 * 5)",
  "10 - 20 + 30 * 2 / 10",  
  "10 - ((4 * 5) - (5 * 6)) * 2 / 10",  
  "10 - ((4 * (2 + 3)) - (5 * 6)) * 2 / 10",  
  "1 + 5 * 2 + 12 * 2 * 2",
  "10 + 13 - 5 * 3 + 12 / 3 + 3"
].forEach (expr => console .log (`${expr} ==> ${evalNumericExpr (expr)}`))

Большим преимуществом этого подхода является то, что его можно легко распространить на любые математические операторы, которые мы выберем. И есть место для дальнейшего упрощения.

Обновление 2

Я был не очень доволен кодом из предыдущего обновления. Эта версия кажется мне более чистой, а также добавляет возведение в степень. Вот первый шаг к этому:

const evalNumericExpr = (() => {
  const ops = [
    [   // grouping
      /\(([^()]+)\)/,
      (evaluator, subexpr) => evaluator(subexpr)
    ], [ //exponentiation
      /([-+]?\d+)\s*[\^]\s*([-+]?\d+)([^\^]*)$/,
      (_, base, exp, rest) => `${base ** exp}${rest}`
    ], [ // multiplication, divide, remainder
      /([-+]?\d+)\s*([*%\/])\s*([-+]?\d+)/, 
      ((ops) => ((_, a, op, b) => ops [op] (Number (a), Number (b))))(
        {'*': (a, b) => a * b, '/': (a, b) => a / b, '%': (a, b) => a % b}
      )
    ], [ // addition, subtraction
      /([-+]?\d+)\s*([+-])\s*([-+]?\d+)/,
      ((ops) => ((_, a, op, b) => ops [op] (Number (a), Number (b))))(
        {'+': (a, b) => a + b, '-': (a, b) => a - b}
      )
    ]
  ]
  const evaluator = (expr) => Number(ops .reduce(
    (expr, [regex, fn]) => regex .test (expr) 
      ? evaluator(expr .replace (regex, (...args) => fn (evaluator, ...args .slice (1)))) 
      : expr,
    expr
  ))
  return evaluator
})()

// Test
; [
  "1 + 3", 
  "7 - 2", 
  "2 * 6", 
  "12 / 4", 
  "2 + 4 + 7", 
  "5 * 2 + 10",
  "10 - 20 + 30 * 2 / 10",  
  "1 + 5 * 2 + 12 * 2 * 2",
  "10 + 13 - 5 * 3 + 12 / 3 + 3",
  "10 + (13 - 5) * 3 + 12 / 3 + 3",
  "5 * (4 + (2 * (1 + 1 + 1)))",
  "5 ^ 2",
  "5 ^ 2 * 2",
  "2 ^ 3 ^ 2", // Note: should parse as `2 ^ (3 ^ 2)`, not `(2 ^ 3) ^ 2`
  "2 ^ 3 ^ 2 + 3 ^ 3 * 2",
] 
.forEach (expr => console .log (`${expr} ==> ${evalNumericExpr (expr)}`))

Мы работаем, связывая регулярное выражение с функцией, которая будет использоваться как обратный вызов replace вместе с этим регулярным выражением. Каждая такая пара запускается повторно до тех пор, пока на входе не останется совпадений.

Сначала он обрабатывает группировку с помощью круглых скобок (изнутри наружу), затем возведение в степень, затем умножение, деление и остатки и, наконец, сложение и вычитание. Этот порядок основан на стандартном приоритете операторов JS диаграмма. Группировка скобок повторяется внутри перед продолжением, и все функции повторяются в оставшемся выражении. Обратите внимание, что возведение в степень, которое правильно связывает, требует немного дополнительной работы, включая оставшуюся часть строки в качестве группы захвата, проверяя, что она не включает никаких операторов возведения в степень; это может быть лучше написано с негативным прогнозом, но я не очень разбираюсь в регулярных выражениях. Также обратите внимание, что я использую курсор (^) для оператора возведения в степень; его должно быть достаточно легко заменить на двойную звездочку (**), если это предпочтительнее.

Для сложного выражения это может происходить следующим образом:

2 ^ 3 ^ (4 ^ 2 - 5 * 3 + 1) - (((2 + 2) * (2 * 5) ^ 2) + (2 * 5 * 7))
         4 ^ 2 - 5 * 3 + 1                                             // grouping
           16  - 5 * 3 + 1                                             // exponentiation
           16 - 15     + 1                                             // multiplication
              1        + 1                                             // subtraction 
                    2                                                  // addition 
2 ^ 3 ^             2       - (((2 + 2) * (2 * 5) ^ 2) + (2 * 5 * 7))
                                 2 + 2                                 // grouping
                                   4                                   // addition
2 ^ 3 ^             2       - ((   4 *    (2 * 5) ^ 2) + (2 * 5 * 7))
                                           2 * 5                       // grouping
                                             10                        // multiplication
2 ^ 3 ^             2       - ((   4 *       10   ^ 2) + (2 * 5 * 7))
                                   4 *       10   ^ 2                  // grouping
                                   4 *          100                    // exponentiation
                                          400                          // multiplication 
2 ^ 3 ^             2       - (           400          + (2 * 5 * 7))
                                                          2 * 5 * 7    // grouping
                                                           10   * 7    // multiplication
                                                              70       // multiplication  
2 ^ 3 ^             2       - (           400          +      70)
                                          400          +      70       // grouping
                                                    470                // addition
2 ^ 3 ^             2       -                       470                
2 ^ 9                       -                       470                // expoentiation
512                         -                       470                // exponentiation
                            42                                         // subtraction
person Scott Sauyet    schedule 13.12.2019

Вы были очень близки. Это должно быть немного сложнее.

Прочтите комментарии внутри кода, чтобы понять, как он работает:

function evaluate(str) { 
  if (str.length === 0) {
    return ""
  }

  // Function to apply the operator from right to left
  const applyOperator = (operator, left, right) => {
    result = left;

    switch(operator) {
      case '+':
        result += right;
        break;
      case '-':
        result -= right;
        break;
      case '*':
        result *= right;
        break;
      case '/':
        // Avoid division by zero
        if(right !== 0) {
          result /= right;
        }
        break;
    }

    return result;
  }
  
  let result = 0;
  let numbers = "";
  let operator = null;

  for (let i = 0; i < str.length; i++) {
    let c = str[i]; // Isolate the character
    let isLast = i === str.length - 1; // Flag to check if we're on the last character

    // Ignore spaces or tabs
    if (c === ' ' || c === '\t') {
      continue;
    }

    // Check if c is a number
    if (!isNaN(parseInt(c))) {
      // If it's a number add it to the number builder
      numbers += c;

      // If it's not the last character then continue to the next character
      if(!isLast) {
        continue;
      }
    } 
    
    // Convert the numbers stack into an integer and reset the stack
    let number = parseInt(numbers);
    numbers = '';
    
    // If there was no operator before,
    // then just set the result with the number and store the operator for the next calculation
    if(operator === null) {
      result = number;
      operator = c;
    } else {
      // Apply the previous operator the the result using the number
      result = applyOperator(operator, result, number);
      // Store the current operator for the next calculation
      operator = c;
    }
  }

  return result;
}

document.getElementById('results').textContent = 
[
  "1 + 1",
  "1 - 1",
  "1 * 1",
  "1 / 1",
  "2 + 4 + 7",
  "5 - 7",
  "5 * 2 + 10",
  "10 - 20 + 30 * 2 / 10"
].map(exp => `${exp} = ${evaluate(exp)}`).join('\n');
<pre id="results"></pre>

ИЗМЕНИТЬ

Я не думаю, что мы пытаемся реализовать здесь какой-то механизм компиляции / интерпретации, но для результата теста вот версия, которая выполняет каждую арифметическую операцию в правильном порядке *, /, -, +:

function evaluate(str) { 
  if (str.length === 0) {
    return ""
  }

  // Function to apply the operator from right to left
  const applyOperator = (operator, left, right) => {
    result = left;

    switch(operator) {
      case '+':
        result += right;
        break;
      case '-':
        result -= right;
        break;
      case '*':
        result *= right;
        break;
      case '/':
        // Avoid division by zero
        if(right !== 0) {
          result /= right;
        }
        break;
    }

    return result;
  }

  const passApply = (exp, opApply) => {
    let result = 0;
    let numbers = "";
    let operator = null;
    let prevWasOp = false;
    let sign = '';

    let parsed = '';

    for (let i = 0; i < exp.length; i++) {
      let c = exp[i]; // Isolate the character
      let isLast = i === exp.length - 1; // Flag to check if we're on the last character

      // Ignore spaces or tabs
      if (c === ' ' || c === '\t') {
        continue;
      }

      // Check if c is a number
      if (!isNaN(parseInt(c))) {
        // If it's a number add it to the number builder
        numbers += c;
        prevWasOp = false;

        // If it's not the last character then continue to the next character
        if(!isLast) {
          continue;
        }
      } else if(prevWasOp || i === 0) {
        // Checked for signed number
        if(/[\+-]/.test(c)) {
          sign = c;
          continue;
        }
        prevWasOp = false;
      }
      
      // Convert the numbers stack into an integer and reset the stack
      let number = parseInt(`${sign}${numbers}`);

      // Reset the sign if there was any
      sign = '';

      // If there was no operator before,
      // then just set the result with the number and store the operator for the next calculation
      if(operator === null) {
        result = number;
        operator = c;
        if(opApply !== operator) {
          parsed += `${numbers}${operator}`;
          result = 0;
        }
      } else {
        if(opApply === operator) {
          // Apply the previous operator the the result using the number
          result = applyOperator(operator, result, number);
          // Store the current operator for the next calculation
          
          if(c !== opApply) {
            parsed += `${result}`;
            if(!isLast) {
              parsed += `${c}`;
            }
            result = 0;
          }
          operator = c;
        } else {          
          if(c !== opApply) {
            parsed += `${numbers}`;
            if(!isLast) {
              parsed += `${c}`;
            }
          }
          operator = c;
          result = number;
        }
      }

      numbers = '';
      prevWasOp = ['+', '-', '*', '/'].indexOf(c) >= 0;
    }

    return parsed;
  }

  // Exeture each operator pass
  const mulPass = passApply(str, '*');
  const divPass = passApply(mulPass, '/');
  const subPass = passApply(divPass, '-');
  const addPass = passApply(subPass, '+');

  // Convert result to int and return the result
  return parseInt(result);
}

document.getElementById('results').textContent = 
[
  "1 + 1",
  "1 - 1",
  "1 * 1",
  "1 / 1",
  "2 + 4 + 7",
  "5 - 7",
  "5 * 2 + 10",
  "10 - 20 + 30 * 2 / 10",
  "1 + 5 * 2 + 12 * 2 * 2",
  "10 + 13 - 5 * 3 + 12 / 3 + 3"
].map(exp => {
  const result = evaluate(exp);
  return `${exp} = ${result}   eval(${result === eval(exp) ? 'OK' : 'ERROR'})`;
}).join('\n');
<pre id="results"></pre>

person Christos Lytras    schedule 13.12.2019
comment
Обратите внимание, что eval('10 - 20 + 30 * 2 / 10') возвращает -4, а не 4. Может ли быть ошибка в приоритете оператора? - person customcommander; 13.12.2019
comment
@customcommander, конечно, делает. Существует определенный порядок, в котором JS и многие языки выполняют арифметические операции (en.wikipedia.org/wiki/Order_of_operations), и для реализации этого нам нужен гораздо более сложный подход, который, я думаю, OP не нужен. Кроме того, наряду с порядком арифметических операций, эта simple функция не поддерживает круглые скобки. - person Christos Lytras; 13.12.2019
comment
Я не уверен, что вижу какую-либо образовательную пользу в показе неправильных примеров. Это сбило бы читателя с толку. Вы должны хотя бы упомянуть ограничения вашей реализации и предложить возможные улучшения. Вам решать. - person customcommander; 13.12.2019
comment
Вы можете получить большую образовательную пользу, прочитав код и увидев, как выполняется анализ простых арифметических вычислений, даже с некоторыми частично неверными результатами. - person Christos Lytras; 13.12.2019
comment
@customcommander: если OP действительно пытается вычислить числовые выражения, то потребуется полноценный числовой синтаксический анализатор. Если вместо этого цель состоит в том, чтобы немного узнать о синтаксическом анализе и, возможно, создать что-то эквивалентное классическому четырехфункциональному калькулятору, то этот подход весьма полезен. - person Scott Sauyet; 13.12.2019