阶梯 pri / 候选 / 资产交付特征等改成界面用词;同步断言。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
/**
|
|
* 集号偏移表达式的白名单求值器(禁止 eval/Function)。
|
|
* 语法:数字、EP 占位符、+ - * / // % ** 和括号。
|
|
* 省略 EP 时按 MP 约定补全:`-12` → `EP-12`,`+1` → `EP+1`,`*2` → `EP*2`。
|
|
*/
|
|
export class OffsetExpressionError extends Error {
|
|
override name = "OffsetExpressionError";
|
|
}
|
|
|
|
type Tok = { t: "num"; v: number } | { t: "ep" } | { t: "op"; v: string } | { t: "lp" } | { t: "rp" };
|
|
|
|
function lex(src: string): Tok[] {
|
|
const out: Tok[] = [];
|
|
let i = 0;
|
|
while (i < src.length) {
|
|
const c = src[i]!;
|
|
if (c === " ") {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (/\d/.test(c)) {
|
|
let j = i;
|
|
while (j < src.length && /\d/.test(src[j]!)) j++;
|
|
out.push({ t: "num", v: Number(src.slice(i, j)) });
|
|
i = j;
|
|
continue;
|
|
}
|
|
if (src.slice(i, i + 2).toUpperCase() === "EP") {
|
|
out.push({ t: "ep" });
|
|
i += 2;
|
|
continue;
|
|
}
|
|
const two = src.slice(i, i + 2);
|
|
if (two === "//" || two === "**") {
|
|
out.push({ t: "op", v: two });
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if ("+-*/%".includes(c)) {
|
|
out.push({ t: "op", v: c });
|
|
i++;
|
|
continue;
|
|
}
|
|
if (c === "(") out.push({ t: "lp" });
|
|
else if (c === ")") out.push({ t: "rp" });
|
|
else throw new OffsetExpressionError(`非法字符:${c}`);
|
|
i++;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function normalizeOffsetExpression(expr: string): string {
|
|
const e = expr.trim();
|
|
if (e === "") throw new OffsetExpressionError("空表达式");
|
|
if (/ep/i.test(e)) return e;
|
|
return /^[+\-*/%]/.test(e) ? `EP${e}` : `EP+${e}`;
|
|
}
|
|
|
|
export function evalEpisodeOffset(expr: string, ep: number): number {
|
|
if (expr.length > 64) throw new OffsetExpressionError("表达式过长");
|
|
const toks = lex(normalizeOffsetExpression(expr));
|
|
let pos = 0;
|
|
const peek = () => toks[pos];
|
|
// expr := term (('+'|'-') term)*
|
|
const parseExpr = (): number => {
|
|
let v = parseTerm();
|
|
for (let p = peek(); p?.t === "op" && (p.v === "+" || p.v === "-"); p = peek()) {
|
|
pos++;
|
|
const r = parseTerm();
|
|
v = p.v === "+" ? v + r : v - r;
|
|
}
|
|
return v;
|
|
};
|
|
const parseTerm = (): number => {
|
|
let v = parsePow();
|
|
for (let p = peek(); p?.t === "op" && ["*", "/", "//", "%"].includes(p.v); p = peek()) {
|
|
pos++;
|
|
const r = parsePow();
|
|
if ((p.v === "/" || p.v === "//" || p.v === "%") && r === 0) throw new OffsetExpressionError("除零");
|
|
if (p.v === "*") v *= r;
|
|
else if (p.v === "/") v /= r;
|
|
else if (p.v === "//") v = Math.floor(v / r);
|
|
else v = ((v % r) + r) % r;
|
|
}
|
|
return v;
|
|
};
|
|
const parsePow = (): number => {
|
|
const base = parseUnary();
|
|
const p = peek();
|
|
if (p?.t === "op" && p.v === "**") {
|
|
pos++;
|
|
return base ** parsePow();
|
|
}
|
|
return base;
|
|
};
|
|
const parseUnary = (): number => {
|
|
const p = peek();
|
|
if (p?.t === "op" && (p.v === "-" || p.v === "+")) {
|
|
pos++;
|
|
const v = parseUnary();
|
|
return p.v === "-" ? -v : v;
|
|
}
|
|
return parseAtom();
|
|
};
|
|
const parseAtom = (): number => {
|
|
const p = toks[pos++];
|
|
if (!p) throw new OffsetExpressionError("表达式不完整");
|
|
if (p.t === "num") return p.v;
|
|
if (p.t === "ep") return ep;
|
|
if (p.t === "lp") {
|
|
const v = parseExpr();
|
|
if (toks[pos++]?.t !== "rp") throw new OffsetExpressionError("括号不匹配");
|
|
return v;
|
|
}
|
|
throw new OffsetExpressionError("语法错误");
|
|
};
|
|
const v = parseExpr();
|
|
if (pos !== toks.length) throw new OffsetExpressionError("多余的内容");
|
|
if (!Number.isFinite(v)) throw new OffsetExpressionError("结果不是有限数");
|
|
return Math.trunc(v);
|
|
}
|