mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
Refactor evaluator by moving switch cases to handler functions
Extracted blocks of code in the massive `evaluate` switch statement in `src/engine/evaluator.ts` into dedicated handler functions: - `evaluateConst` - `evaluateRef` - `evaluateMath` - `evaluateUnitMod` - `evaluateSeq` This significantly reduces the complexity of `evaluate` while preserving all logic and TypeScript type narrowing. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
c958f30662
commit
c7798a75e2
1 changed files with 109 additions and 78 deletions
|
|
@ -36,69 +36,27 @@ export function evaluate(node: Node, ctx: EvalCtx): Value {
|
||||||
case "num":
|
case "num":
|
||||||
return qty(node.v, null, node.repr);
|
return qty(node.v, null, node.repr);
|
||||||
case "const":
|
case "const":
|
||||||
switch (node.name) {
|
return evaluateConst(node);
|
||||||
case "pi": return qty(PI);
|
case "var":
|
||||||
case "e": return qty(E);
|
case "xref":
|
||||||
case "half": return qty(new Decimal("0.5"));
|
case "unknown":
|
||||||
case "onehalf": return qty(new Decimal("1.5"));
|
return evaluateRef(node, ctx);
|
||||||
}
|
|
||||||
case "var": {
|
|
||||||
const v = ctx.vars.get(node.name);
|
|
||||||
if (!v) throw new EvalError(`unknown variable ${node.name}`);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
case "date":
|
case "date":
|
||||||
return evalDateWord(node.word);
|
return evalDateWord(node.word);
|
||||||
case "datelit":
|
case "datelit":
|
||||||
return { kind: "date", ms: node.ms, hasTime: false };
|
return { kind: "date", ms: node.ms, hasTime: false };
|
||||||
case "xref": {
|
|
||||||
const res = ctx.resolveXRef?.(node.sheet, node.key);
|
|
||||||
if (!res || !res.ok) {
|
|
||||||
throw new XRefError(res && !res.ok ? res.reason : `sheet "${node.sheet}" not found`);
|
|
||||||
}
|
|
||||||
return res.value;
|
|
||||||
}
|
|
||||||
case "unknown": {
|
|
||||||
const xv = ctx.vars.get("__x__");
|
|
||||||
if (!xv) throw new EvalError("? used outside goal seek");
|
|
||||||
return xv;
|
|
||||||
}
|
|
||||||
case "goalseek":
|
case "goalseek":
|
||||||
return evalGoalSeek(node.lhs, node.rhs, ctx);
|
return evalGoalSeek(node.lhs, node.rhs, ctx);
|
||||||
case "agg":
|
case "agg":
|
||||||
return evalAgg(node.name, ctx);
|
return evalAgg(node.name, ctx);
|
||||||
case "neg": {
|
case "neg":
|
||||||
const v = evaluate(node.x, ctx);
|
case "pct":
|
||||||
if (v.kind === "quantity") return { ...v, value: v.value.neg() };
|
case "fact":
|
||||||
if (v.kind === "percent") return pct(v.value.neg());
|
case "scale":
|
||||||
throw new EvalError("cannot negate a date");
|
return evaluateMath(node, ctx);
|
||||||
}
|
case "unit":
|
||||||
case "pct": {
|
case "curr":
|
||||||
const v = evaluate(node.x, ctx);
|
return evaluateUnitMod(node, ctx);
|
||||||
if (v.kind === "quantity" && !v.unit) return pct(v.value);
|
|
||||||
if (v.kind === "percent") return v;
|
|
||||||
throw new EvalError("% needs a plain number");
|
|
||||||
}
|
|
||||||
case "fact": {
|
|
||||||
const v = asScalar(evaluate(node.x, ctx));
|
|
||||||
return qty(factorial(v));
|
|
||||||
}
|
|
||||||
case "unit": {
|
|
||||||
const v = evaluate(node.x, ctx);
|
|
||||||
return attachUnit(v, node.unit);
|
|
||||||
}
|
|
||||||
case "curr": {
|
|
||||||
const unit = ctx.reg.makeCurrencyUnit(node.code);
|
|
||||||
if (!unit) throw new EvalError(`no rate for ${node.code}`);
|
|
||||||
const v = evaluate(node.x, ctx);
|
|
||||||
return attachUnit(v, unit);
|
|
||||||
}
|
|
||||||
case "scale": {
|
|
||||||
const v = evaluate(node.x, ctx);
|
|
||||||
if (v.kind === "quantity") return { ...v, value: v.value.mul(node.mult) };
|
|
||||||
if (v.kind === "percent") return pct(v.value.mul(node.mult));
|
|
||||||
throw new EvalError("cannot scale a date");
|
|
||||||
}
|
|
||||||
case "bin":
|
case "bin":
|
||||||
return evalBin(node.op, evaluate(node.l, ctx), evaluate(node.r, ctx), ctx);
|
return evalBin(node.op, evaluate(node.l, ctx), evaluate(node.r, ctx), ctx);
|
||||||
case "bit":
|
case "bit":
|
||||||
|
|
@ -109,29 +67,8 @@ export function evaluate(node: Node, ctx: EvalCtx): Value {
|
||||||
return evalConv(evaluate(node.x, ctx), node.target, ctx);
|
return evalConv(evaluate(node.x, ctx), node.target, ctx);
|
||||||
case "call":
|
case "call":
|
||||||
return evalCall(node.name, node.args.map((a) => evaluate(a, ctx)), ctx);
|
return evalCall(node.name, node.args.map((a) => evaluate(a, ctx)), ctx);
|
||||||
case "seq": {
|
case "seq":
|
||||||
let acc: Value | null = null;
|
return evaluateSeq(node, ctx);
|
||||||
for (const item of node.items) {
|
|
||||||
let v: Value;
|
|
||||||
try {
|
|
||||||
v = evaluate(item, ctx);
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
acc?.kind === "quantity" && v.kind === "quantity" &&
|
|
||||||
acc.unit && v.unit && acc.unit.dimension === v.unit.dimension &&
|
|
||||||
acc.unit.dimension !== "scalar"
|
|
||||||
) {
|
|
||||||
// "2 hours 30 min", "5 ft 4 in" — adjacent same-dimension quantities add up
|
|
||||||
acc = addQ(acc, v);
|
|
||||||
} else {
|
|
||||||
acc = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!acc) throw new EvalError("empty expression");
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -644,3 +581,97 @@ function factorial(n: Decimal): Decimal {
|
||||||
for (let i = 2; i <= n.toNumber(); i++) acc = acc.mul(i);
|
for (let i = 2; i <= n.toNumber(); i++) acc = acc.mul(i);
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function evaluateConst(node: { k: "const", name: "pi" | "e" | "half" | "onehalf" }): Value {
|
||||||
|
switch (node.name) {
|
||||||
|
case "pi": return qty(PI);
|
||||||
|
case "e": return qty(E);
|
||||||
|
case "half": return qty(new Decimal("0.5"));
|
||||||
|
case "onehalf": return qty(new Decimal("1.5"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateRef(node: Node, ctx: EvalCtx): Value {
|
||||||
|
if (node.k === "var") {
|
||||||
|
const v = ctx.vars.get(node.name);
|
||||||
|
if (!v) throw new EvalError(`unknown variable ${node.name}`);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
if (node.k === "xref") {
|
||||||
|
const res = ctx.resolveXRef?.(node.sheet, node.key);
|
||||||
|
if (!res || !res.ok) {
|
||||||
|
throw new XRefError(res && !res.ok ? res.reason : `sheet "${node.sheet}" not found`);
|
||||||
|
}
|
||||||
|
return res.value;
|
||||||
|
}
|
||||||
|
if (node.k === "unknown") {
|
||||||
|
const xv = ctx.vars.get("__x__");
|
||||||
|
if (!xv) throw new EvalError("? used outside goal seek");
|
||||||
|
return xv;
|
||||||
|
}
|
||||||
|
throw new EvalError("invalid ref node");
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateMath(node: Node, ctx: EvalCtx): Value {
|
||||||
|
if (node.k === "neg") {
|
||||||
|
const v = evaluate(node.x, ctx);
|
||||||
|
if (v.kind === "quantity") return { ...v, value: v.value.neg() };
|
||||||
|
if (v.kind === "percent") return pct(v.value.neg());
|
||||||
|
throw new EvalError("cannot negate a date");
|
||||||
|
}
|
||||||
|
if (node.k === "pct") {
|
||||||
|
const v = evaluate(node.x, ctx);
|
||||||
|
if (v.kind === "quantity" && !v.unit) return pct(v.value);
|
||||||
|
if (v.kind === "percent") return v;
|
||||||
|
throw new EvalError("% needs a plain number");
|
||||||
|
}
|
||||||
|
if (node.k === "fact") {
|
||||||
|
const v = asScalar(evaluate(node.x, ctx));
|
||||||
|
return qty(factorial(v));
|
||||||
|
}
|
||||||
|
if (node.k === "scale") {
|
||||||
|
const v = evaluate(node.x, ctx);
|
||||||
|
if (v.kind === "quantity") return { ...v, value: v.value.mul(node.mult) };
|
||||||
|
if (v.kind === "percent") return pct(v.value.mul(node.mult));
|
||||||
|
throw new EvalError("cannot scale a date");
|
||||||
|
}
|
||||||
|
throw new EvalError("invalid math node");
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateUnitMod(node: Node, ctx: EvalCtx): Value {
|
||||||
|
if (node.k === "unit") {
|
||||||
|
const v = evaluate(node.x, ctx);
|
||||||
|
return attachUnit(v, node.unit);
|
||||||
|
}
|
||||||
|
if (node.k === "curr") {
|
||||||
|
const unit = ctx.reg.makeCurrencyUnit(node.code);
|
||||||
|
if (!unit) throw new EvalError(`no rate for ${node.code}`);
|
||||||
|
const v = evaluate(node.x, ctx);
|
||||||
|
return attachUnit(v, unit);
|
||||||
|
}
|
||||||
|
throw new EvalError("invalid unit mod node");
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateSeq(node: { k: "seq", items: Node[] }, ctx: EvalCtx): Value {
|
||||||
|
let acc: Value | null = null;
|
||||||
|
for (const item of node.items) {
|
||||||
|
let v: Value;
|
||||||
|
try {
|
||||||
|
v = evaluate(item, ctx);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
acc?.kind === "quantity" && v.kind === "quantity" &&
|
||||||
|
acc.unit && v.unit && acc.unit.dimension === v.unit.dimension &&
|
||||||
|
acc.unit.dimension !== "scalar"
|
||||||
|
) {
|
||||||
|
// "2 hours 30 min", "5 ft 4 in" — adjacent same-dimension quantities add up
|
||||||
|
acc = addQ(acc, v);
|
||||||
|
} else {
|
||||||
|
acc = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!acc) throw new EvalError("empty expression");
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue