功能说明
tokenizeExpression 把表达式拆成数字、运算符与括号记号,evaluateExpression 使用调度场算法转换为逆波兰序列并求值。支持加减乘除、取模、幂、括号、小数、科学计数法以及一元正负号,不依赖 eval。适合计算器、规则引擎中的简单公式、报表字段运算和配置化阈值表达式。
关键代码
分词器逐字符扫描,数字用正则匹配小数与指数部分,其他符号按单字符识别,遇到未知字符立即报错。解析阶段根据前一个记号判断加减号是否为一元运算符,并给予最高优先级且右结合。运算符入栈前弹出优先级更高或同级左结合的运算符,右括号触发弹栈直到左括号。求值阶段用数字栈执行二元或一元运算,最终必须只剩一个值。
使用注意
该解析器不支持变量、函数调用、隐式乘法或赋值,需要这些能力应扩展记号类型或改用专门表达式库。除零会得到 Infinity,取模零得到 NaN,业务层应自行校验。幂运算右结合,负号与幂的组合按当前优先级解释,界面应提示用户加括号。输入长度应设上限,避免恶意超长表达式消耗资源。
验证方式
测试 1+2*3、带括号的 (1+2)*3、2^3^2 的右结合、-3+5 与 -(2+3) 的一元符号、1.5e2 与 10%3。测试多余右括号、缺少右括号、连续运算符、空表达式与非法字符会抛错。再与 JavaScript 直接计算的结果对比一组随机合法表达式,确认浮点结果在误差范围内一致。
代码示例
// Arithmetic expression parser: tokens, shunting-yard and RPN evaluation.
type Token =
| { type: "number"; value: number }
| { type: "operator"; value: string }
| { type: "paren"; value: "(" | ")" };
const PRECEDENCE: Record<string, number> = {
"+": 1, "-": 1, "*": 2, "/": 2, "%": 2, "^": 3, "u-": 4, "u+": 4,
};
const RIGHT_ASSOCIATIVE = new Set(["^", "u-", "u+"]);
export function tokenizeExpression(input: string): Token[] {
const tokens: Token[] = [];
let index = 0;
while (index < input.length) {
const char = input[index]!;
if (/\s/.test(char)) {
index += 1;
continue;
}
if (/[0-9.]/.test(char)) {
const match = /^\d*\.?\d+(?:[eE][+-]?\d+)?/.exec(input.slice(index));
if (!match) throw new SyntaxError("invalid number at position " + index);
tokens.push({ type: "number", value: Number(match[0]) });
index += match[0].length;
continue;
}
if ("+-*/%^".includes(char)) {
tokens.push({ type: "operator", value: char });
index += 1;
continue;
}
if (char === "(" || char === ")") {
tokens.push({ type: "paren", value: char });
index += 1;
continue;
}
throw new SyntaxError("unexpected character '" + char + "'");
}
return tokens;
}
function evaluateRpn(tokens: Token[]): number {
const stack: number[] = [];
for (const token of tokens) {
if (token.type === "number") {
stack.push(token.value);
continue;
}
const operator = token.value;
if (operator === "u-" || operator === "u+") {
const operand = stack.pop();
if (operand === undefined) throw new SyntaxError("missing unary operand");
stack.push(operator === "u-" ? -operand : operand);
continue;
}
const right = stack.pop();
const left = stack.pop();
if (left === undefined || right === undefined) throw new SyntaxError("missing operand");
const value = operator === "+" ? left + right
: operator === "-" ? left - right
: operator === "*" ? left * right
: operator === "/" ? left / right
: operator === "%" ? left % right
: left ** right;
stack.push(value);
}
if (stack.length !== 1) throw new SyntaxError("invalid expression");
return stack[0]!;
}
export function evaluateExpression(expression: string): number {
const output: Token[] = [];
const operators: string[] = [];
const tokens = tokenizeExpression(expression);
let previous: Token | undefined;
for (const token of tokens) {
if (token.type === "number") {
output.push(token);
} else if (token.type === "operator") {
const unary = (token.value === "-" || token.value === "+")
&& (!previous || previous.type === "operator" || (previous.type === "paren" && previous.value === "("));
const operator = unary ? "u" + token.value : token.value;
while (operators.length > 0) {
const top = operators[operators.length - 1]!;
if (top === "(") break;
const higher = PRECEDENCE[top]! > PRECEDENCE[operator]!
|| (PRECEDENCE[top] === PRECEDENCE[operator] && !RIGHT_ASSOCIATIVE.has(operator));
if (!higher) break;
output.push({ type: "operator", value: operators.pop()! });
}
operators.push(operator);
} else if (token.value === "(") {
operators.push("(");
} else {
while (operators.length > 0 && operators[operators.length - 1] !== "(") {
output.push({ type: "operator", value: operators.pop()! });
}
if (operators.length === 0) throw new SyntaxError("unbalanced parentheses");
operators.pop();
}
previous = token;
}
while (operators.length > 0) {
const operator = operators.pop()!;
if (operator === "(") throw new SyntaxError("unbalanced parentheses");
output.push({ type: "operator", value: operator });
}
return evaluateRpn(output);
}