Skip to content

表达式计算

ts
// 符号优先级
const precedence = {
  "+": 1,
  "-": 1,
  "*": 2,
  "/": 2,
  "(": 0,
} as const;

function mathEval(expr: string): number {
  const tokens = expr.replace(/\s+/g, "").match(/(\d+(?:\.\d+)?|\*\*|[-+*/()!])/g) || [];
  const operands: number[] = [];
  const operators: string[] = [];
  // 记录上一个 token 的类型:数字 | 运算符 | '(' | 空
  let prevTokenType: "number" | "operator" | "parenOpen" | "none" = "none";
  // 处理负号
  let sign = 1;

  for (const token of tokens) {
    // 如果是数字,数字入栈
    if (/\d+/.test(token)) {
      operands.push(sign * parseFloat(token));
      sign = 1;
      prevTokenType = "number";
    }
    // 如果是 '(',符号入栈
    else if (token === "(") {
      operators.push(token);
      prevTokenType = "parenOpen";
    }
    // 如果是 ')',符号出栈,直到 '('
    else if (token === ")") {
      while (operators.length > 0 && operators[operators.length - 1] !== "(") {
        applyTopOperator(operands, operators);
      }
      operators.pop(); // 移除 '('
      prevTokenType = "number"; // ')' 后视为数字
    }
    // 如果是运算符
    else {
      // 处理阶乘
      if (token === "!") {
        if (prevTokenType !== "number") {
          throw new Error(`Invalid expression: "${token}" operator must follow a number`);
        }
        const num = operands.pop()!;
        if (num < 0) {
          throw new Error(`Invalid expression: factorial of negative number "${num}!"`);
        }
        let result = 1;
        for (let i = 2; i <= num; i++) {
          result *= i;
        }
        operands.push(result);
        prevTokenType = "number";
        continue;
      }
      // 处理负号
      else if (token === "-") {
        // 负号前面不是数字,也不是运算符,视为 0 参与运算
        if (prevTokenType === "none" || prevTokenType === "parenOpen") {
          operands.push(0);
        }
        // 负号前面是运算符,视为负号
        else if (prevTokenType === "operator") {
          if (sign === -1) {
            throw new Error(
              `Invalid expression: two consecutive operators "${operators[operators.length - 1]}${token}"`,
            );
          }
          sign = -1;
          continue;
        }
      }
      // 之前也是运算符,说明表达式错误
      else if (prevTokenType === "operator") {
        throw new Error(
          `Invalid expression: two consecutive operators "${operators[operators.length - 1]}${token}"`,
        );
      }
      // 处理符号运算
      while (
        operators.length > 0 &&
        precedence[operators[operators.length - 1]] >= precedence[token]
      ) {
        if (operators[operators.length - 1] === "**" && token === "**") {
          // 处理幂运算的右结合性
          break;
        }
        applyTopOperator(operands, operators);
      }
      operators.push(token);
      prevTokenType = "operator";
    }
  }

  // 处理剩余的运算符
  while (operators.length > 0) {
    applyTopOperator(operands, operators);
  }

  return operands.pop()!;
}

function applyTopOperator(operands: number[], operators: string[]): void {
  const op = operators.pop()!;
  const right = operands.pop()!;
  const left = operands.pop()!;
  operands.push(applyOp(op, left, right));
}

function applyOp(op: string, left: number, right: number): number {
  switch (op) {
    case "+":
      return left + right;
    case "-":
      return left - right;
    case "*":
      return left * right;
    case "/":
      return left / right;
    case "**":
      return Math.pow(left, right);
    default:
      throw new Error(`Invalid operator: ${op}`);
  }
}

function testMathEval() {
  const tests = [
    { input: "3 + 5", expected: 8 },
    { input: "10 - 2 * 3", expected: 4 },
    { input: "(1 + 2) * (3 + 4)", expected: 21 },
    { input: "3 + (2 * 4)", expected: 11 },
    { input: "10 / (5 - 3)", expected: 5 },
    { input: "2 + 3 * (4 - 1)", expected: 11 },
    { input: "5 * (6 + 2) - 3", expected: 37 },
    { input: "10 + (2 * (3 + 4))", expected: 24 },
    { input: "((2 + 3) * (4 - 1)) / (5 - 2)", expected: 5 },
    { input: "1 + (2 * (3 + (4 - (5 + (6 - (7 + (8 - (9))))))))", expected: 5 },
    { input: "-1 + (-2) * (-3)", expected: 5 },
    { input: "-(1 + 2)", expected: -3 },
    { input: "-(1 + (-2))", expected: 1 },
    { input: "-(1) + (-2)", expected: -3 },
    { input: "3 + -2", expected: 1 },
    { input: "3 - -2", expected: 5 },
    { input: "3 * -2", expected: -6 },
    { input: "3 / -2", expected: -1.5 },
    { input: "-3 + 2", expected: -1 },
    { input: "-3 - 2", expected: -5 },
    { input: "-3.5 + 2.5", expected: -1 },
    { input: "-3.5 * -2.5", expected: 8.75 },
    { input: "3! ** 3!", expected: 46656 },
  ];
  for (const { input, expected } of tests) {
    const result = mathEval(input);
    if (result !== expected) {
      console.error(`❌ Test failed for input "${input}": expected ${expected}, got ${result}`);
    } else {
      console.log(`✅ Test passed for input "${input}": got ${result}`);
    }
  }
}

testMathEval();