/** * 集号偏移表达式的白名单求值器(禁止 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); }