【问题标题】:How to implement build in function .eval() with recursive function已解决:如何使用递归函数实现内置函数 .eval()
【发布时间】:2019-12-13 14:57:56
【问题描述】:

嘿编码员,

我有一个字符串“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 会拯救我的一天,但在这种情况下,我需要解决这个问题,谢谢。

【问题讨论】:

  • 几个问题:你必须使用递归吗?输入是否总是像您的示例一样简单,即“数字运算数字”,没有更复杂的?您需要多位数的号码吗?
  • 嗨,谢谢你的时间,在测试用例中可能是多位数

标签: javascript function recursion eval evaluate


【解决方案1】:

另一种方法是将字符串转换为可以作为堆栈评估的数组,然后对该堆栈进行简单的评估。例如,我们可以将"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 来为归约添加其他arity 操作(尽管将字符串转换为堆栈格式会更棘手)。如果我们想处理十进制数字,我们可以将正则表达式更改为 /([-+*\/])(\s)*(\-?\d+(:?\.\d+)?)/g。

祝贺我们。我们刚刚写了Forth interpreter的开头!


更新

这个问题专门询问了如何递归地执行此操作,我写了一个递归版本,总体上比上面简单。但后来我意识到它可以很容易地扩展到处理括号并尊重运算符优先级。它不再一定更简单,但它是一种有趣的方法,我们可以很容易地用其他运算符和不同的优先级扩展它:

// 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 operator precedence 图表。括号的分组在继续之前在内部重复出现,并且所有函数都在剩余的表达式上重复出现。请注意,与右关联的幂运算需要做一些额外的工作,包括将字符串的其余部分作为捕获组,测试它不包含任何幂运算符;这可能会更好地写成负面的前瞻性,但我不是一个正则表达式专家。另请注意,我将插入符号 (^) 用于求幂运算符;如果愿意,更改为双星号 (**) 应该很容易。

对于一个复杂的表达式,它可能会这样处理:

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

【讨论】:

    【解决方案2】:

    试试这个代码

    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
    

    【讨论】:

      【解决方案3】:

      你们很亲密。它需要更复杂一些。

      请阅读代码中的 cmets 以了解其工作原理:

      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');
      &lt;pre id="results"&gt;&lt;/pre&gt;

      编辑

      我不认为我们在这里尝试实现某种编译/解释引擎,但为了测试结果,这里是一个以正确顺序执行每个算术运算的版本*, /, -, +:

      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');
      &lt;pre id="results"&gt;&lt;/pre&gt;

      【讨论】:

      • 请注意 eval('10 - 20 + 30 * 2 / 10') 返回 -4 而不是 4。会不会是运算符优先级有错误?
      • @customcommander 当然可以。 JS 和许多语言执行算术运算(en.wikipedia.org/wiki/Order_of_operations)有一定的顺序,为了实现这一点,我们需要一种更复杂的方法,我认为 OP 不需要。除了算术运算顺序,这个simple函数不支持括号。
      • 我不确定展示错误示例是否有任何教育益处。这会使读者感到困惑。您至少应该提及您的实施的限制并提出可能的改进建议。由你决定。
      • 通读代码并了解基本算术计算解析是如何完成的,即使结果部分错误。
      • @customcommander:如果 OP 真的在尝试评估数字表达式,那么一个成熟的数字解析器将是必要的。如果目标是学习一些关于解析的知识,或者做一些相当于老式四函数计算器的东西,那么这种方法非常有用。
      猜你喜欢
      • 2020-12-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多