JS验证四则运算的有效性

908 阅读2分钟

引言

image.png 业务中遇到这样一个场景,有一个指标可通过其他指标进行四则运算组合生成,需要验证该表达式的有效性 该问题可以通过将指标替换为一个数字的方式进而将问题抽象为如何进行四则运算的有效性验证, 比如图中表达式可以替换为1+1

经过一番搜索,找到一种解决方案是通过eval直接执行来达到验证的目的

function valid(expression) {
            try {
                var result = eval(expression);
            } catch(e) {
                return false;
            }
            return true;
        }

但这种方式无法验证多余括号组的情况,比如:

(((1+2))*(((2+3))))
((1+2))*((2+3))
((1+2))

这个时候,其实可以甩给后端,让后端去验证(手动狗头) 自己对这个比较感兴趣,所以自己研究实现了一番

这个验证的难点在于:

1、括号需要匹配,且出现在正确的位置上

2、运算符两侧必须是数字

3、运算符与运算符也不能一起出现

验证思路:

1、遍历表达式,如出现数字,运算符,括号以外的字符,验证失败

2、将表达式依次转化为数字、运算符、括号的数组

3、在该数组的基础上,参考逆波兰表示法求四则运算,将中缀表达式转换为后缀表达式这个过程中,可以验证出 空括号括号不匹配的情况

4、最后再用转换好的后缀表达式来计算求值,求值出错,说明表达式有误

代码实现(此实现不包含负数):


// 四则运算的验证
// const test1 = '((1+2)'
// const test2 = '))1+2'
// const test3 = '(1+2*-(2-3*-)3)'
// const test4 = '1+2-(2(-3*3)'
// const test5 = '1+2-(2-3*)3)'
// const test6 = '1+2*-(2-3*-)3'
// const test7 = '1+2*-(2-3*-)3()'
// const test8 = '((1+2-(2-3)+3))'
// const test9 = '1+323-(2-3*3)'
// console.log(validateArithmeticExpression(test1)) // false
// console.log(validateArithmeticExpression(test2)) // false
// console.log(validateArithmeticExpression(test3)) // false
// console.log(validateArithmeticExpression(test4)) // false
// console.log(validateArithmeticExpression(test5)) // false
// console.log(validateArithmeticExpression(test6)) // false
// console.log(validateArithmeticExpression(test7)) // false
// console.log(validateArithmeticExpression(test8)) // false
// console.log(validateArithmeticExpression(test9)) // true
export const validateArithmeticExpression = (str: string): boolean => {
  type Operation = '+' | '-' | '/' | '*';
  // 将字符串进行拆分、 '123+12-1' => ['123', '+', '12', '-', '1]
  const lexicalAnalysis = (expression) => {
    const symbol = ['(', ')', '+', '-', '*', '/'];
    const re = /\d/;
    const tokens: string[] = [];
    const chars = expression.trim().split('');
    let token = '';
    chars.forEach((c) => {
      if (re.test(c)) {
        token += c;
      } else if (c == ' ' && token) {
        tokens.push(token);
        token = '';
      } else if (symbol.includes(c)) {
        if (token) {
          tokens.push(token);
          token = '';
        }

        tokens.push(c);
      }
    });

    if (token) {
      tokens.push(token);
    }

    return tokens;
  };
  // 将中缀表达式转化为后缀表达式
  const toRPN = (_chars: string[]) => {
    const resultStack: string[] = [];
    const operationStack: string[] = [];
    const operations: Operation[] = ['+', '-', '/', '*'];
    const reg = /[\d\+\-\/\*\(\)]/i;
    const result: {
      RPNexpression: string;
      isValid: boolean;
    } = {
      isValid: true,
      RPNexpression: '',
    };

    for (let i = 0; i < _chars.length; i++) {
      const char = _chars[i];
      // 若不是规定字符,直接报错
      if (!reg.test(char)) {
        result.isValid = false;
        break;
      }

      let operationStackTop = operationStack[operationStack.length - 1];

      switch (char) {
        case '+':
        case '-':
        case '*':
        case '/':
          // 栈顶为空 或者 为( 直接入栈
          if (
            !operationStackTop ||
            !((operations as unknown) as string[]).includes(operationStackTop)
          ) {
            operationStack.push(char);
          } else {
            // 如遇+ - * / ,与栈顶元素比较优先级,若栈顶优先级较高,便将优先级高的栈顶出栈压入S2,并与下一个栈顶比较,直到遇到平级或者(,将遍历元素压入s1中
            while (!isOp1HigherPriority(char, operationStackTop as Operation)) {
              // 栈为空 或者栈顶为(),结束循环
              if (
                !operationStack.length ||
                operationStackTop === '(' ||
                operationStackTop === ')'
              ) {
                break;
              }

              const lowerOperation = operationStack.pop();
              if (lowerOperation) {
                resultStack.push(lowerOperation);
              }
              operationStackTop = operationStack[operationStack.length - 1];
            }
            operationStack.push(char);
          }

          break;
        case '(':
          operationStack.push(char);
          break;
        case ')':
          // 操作符栈中无元素,说明括号不匹配
          if (!operationStack.length) {
            result.isValid = false;
            break;
          }
          if (operationStackTop === '(') {
            result.isValid = false;
            break;
          }
          if (operationStack.length) {
            while (operationStackTop && operationStackTop !== '(') {
              const operation = operationStack.pop();
              if (operation) {
                resultStack.push(operation);
              }
              operationStackTop = operationStack[operationStack.length - 1];
            }
            if (operationStackTop === '(') {
              operationStack.pop();
            }
            // 遍历到栈底,也没有找到左括号,说明没有匹配的左括号
            if (!operationStackTop) {
              result.isValid = false;
              break;
            }
          }
          break;
        // 数字
        default:
          resultStack.push(char);
          break;
      }

      if (i === _chars.length - 1) {
        while (operationStack.length) {
          let stackTop = operationStack.pop();
          if (stackTop === '(' || stackTop === ')') {
            result.isValid = false;
            break;
          }
          if (stackTop) {
            resultStack.push(stackTop);
          }
        }
      }
    }
    result.RPNexpression = resultStack.join('');
    return result;
  };

  // 执行后缀表达式的计算并验证
  const evalRPN = (tokens: string) => {
    try {
      const stack: any[] = [];
      const length = tokens.length;
      for (let i = 0; i < length; i++) {
        const c = tokens[i];
        if (c === '+' || c === '-' || c === '*' || c === '/') {
          const val1 = parseInt(stack.pop());
          const val2 = parseInt(stack.pop());
          if (Number.isNaN(val1) || Number.isNaN(val2)) {
            return {
              validRPN: false,
              calcResult: null,
            };
          }
          switch (c) {
            case '+':
              stack.push(val2 + val1);
              break;
            case '-':
              stack.push(val2 - val1);
              break;
            case '*':
              stack.push(val2 * val1);
              break;
            case '/':
              stack.push(parseInt(`${val2 / val1}`));
              break;
          }
        } else {
          stack.push(c);
        }
        // 如果正确计算到最后,应该只有一个值了
        if (i === length - 1 && stack.length !== 1) {
          return {
            validRPN: false,
            calcResult: null,
          };
        }
      }

      return {
        validRPN: true,
        calcResult: stack[0],
      };
    } catch (error) {
      return {
        validRPN: false,
        calcResult: null,
      };
    }
  };

  // 判断op1的优先级是不是更高
  const isOp1HigherPriority = (op1: Operation, op2: Operation): boolean => {
    if (op1 === '*' || (op1 === '/' && (op2 === '+' || op2 === '-'))) {
      return true;
    }
    return false;
  };

  const charsArr = lexicalAnalysis(str);

  const { isValid, RPNexpression } = toRPN(charsArr);
  if (!isValid) {
    return isValid;
  }

  const { validRPN } = evalRPN(RPNexpression);

  return validRPN;
};

参考文章:

逆波兰表示法求四则运算

简单四则运算及表达式校验

算术表达式解析系列之逆波兰表示法

【JS算法练习】彻底搞懂逆波兰表达式的求法,再复杂的表达式也不用慌~