Files
UHD_Media_Tool/packages/domain/src/release-parser/offset.ts
T
rootandClaude Opus 5.5 0ba947dce3 copy(domain): 命名模板、规则判定、收录版本评估文案改全角标点并去掉内部用语
阶梯 pri / 候选 / 资产交付特征等改成界面用词;同步断言。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-07 01:52:23 +08:00

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);
}