mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-10 21:31:59 +03:00
goal seek: ? * 1.2 = 1000 → 833.33
This commit is contained in:
parent
bbbcc0497c
commit
d5f458dce3
4 changed files with 70 additions and 1 deletions
|
|
@ -302,6 +302,14 @@ describe("dates", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("goal seek", () => {
|
||||||
|
it("? * 1.2 = 1000", () => expect(calc("? * 1.2 = 1000")).toBe("833.33"));
|
||||||
|
it("? + 50 = 200", () => expect(calc("? + 50 = 200")).toBe("150"));
|
||||||
|
it("100 - ? = 30", () => expect(calc("100 - ? = 30")).toBe("70"));
|
||||||
|
it("? / 4 = 5", () => expect(calc("? / 4 = 5")).toBe("20"));
|
||||||
|
it("? - ? = 5 has no solution", () => expect(calc("? - ? = 5")).toBeNull());
|
||||||
|
});
|
||||||
|
|
||||||
describe("extension API", () => {
|
describe("extension API", () => {
|
||||||
it("addUnit (horse from Sample.js)", () => {
|
it("addUnit (horse from Sample.js)", () => {
|
||||||
eng.addUnit({ id: "horse", phrases: "horse, horses, hrs", baseUnitId: "meter", format: "hrs", ratio: 2.4 });
|
eng.addUnit({ id: "horse", phrases: "horse, horses, hrs", baseUnitId: "meter", format: "hrs", ratio: 2.4 });
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,13 @@ export function evaluate(node: Node, ctx: EvalCtx): Value {
|
||||||
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 "unknown": {
|
||||||
|
const xv = ctx.vars.get("__x__");
|
||||||
|
if (!xv) throw new EvalError("? used outside goal seek");
|
||||||
|
return xv;
|
||||||
|
}
|
||||||
|
case "goalseek":
|
||||||
|
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": {
|
||||||
|
|
@ -399,6 +406,42 @@ function evalDateArith(op: string, l: Value, r: Value): Value {
|
||||||
return { ...date, ms, hasTime };
|
return { ...date, ms, hasTime };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function evalGoalSeek(lhsNode: Node, rhsNode: Node, ctx: EvalCtx): Value {
|
||||||
|
const evalF = (x: Decimal): Decimal => {
|
||||||
|
const vars = new Map(ctx.vars);
|
||||||
|
vars.set("__x__", qty(x));
|
||||||
|
const innerCtx: EvalCtx = { ...ctx, vars };
|
||||||
|
const lv = evaluate(lhsNode, innerCtx);
|
||||||
|
const rv = evaluate(rhsNode, innerCtx);
|
||||||
|
const toD = (v: Value) => v.kind === "quantity" ? toBase(v) : new Decimal(0);
|
||||||
|
return toD(lv).minus(toD(rv));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Linear probe: f(0) and f(1) give slope → exact root for linear expressions
|
||||||
|
const f0 = evalF(new Decimal(0));
|
||||||
|
const f1 = evalF(new Decimal(1));
|
||||||
|
const slope = f1.minus(f0);
|
||||||
|
if (!slope.isZero()) {
|
||||||
|
const root = f0.neg().div(slope);
|
||||||
|
if (evalF(root).abs().lt(new Decimal("1e-9"))) return qty(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bisection fallback for nonlinear cases
|
||||||
|
let lo = new Decimal(-1e9);
|
||||||
|
let hi = new Decimal(1e9);
|
||||||
|
let flo = evalF(lo);
|
||||||
|
let fhi = evalF(hi);
|
||||||
|
if (flo.mul(fhi).gt(0)) throw new EvalError("no solution");
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const mid = lo.plus(hi).div(2);
|
||||||
|
if (hi.minus(lo).abs().lt(new Decimal("1e-10"))) return qty(mid);
|
||||||
|
const fm = evalF(mid);
|
||||||
|
if (flo.mul(fm).lte(0)) { hi = mid; fhi = fm; }
|
||||||
|
else { lo = mid; flo = fm; }
|
||||||
|
}
|
||||||
|
return qty(lo.plus(hi).div(2));
|
||||||
|
}
|
||||||
|
|
||||||
function evalAgg(name: "sum" | "avg" | "prev" | "count" | "min" | "max" | "product" | "chart", ctx: EvalCtx): Value {
|
function evalAgg(name: "sum" | "avg" | "prev" | "count" | "min" | "max" | "product" | "chart", ctx: EvalCtx): Value {
|
||||||
const { lineValues, lineKinds, index } = ctx.line;
|
const { lineValues, lineKinds, index } = ctx.line;
|
||||||
if (name === "prev") {
|
if (name === "prev") {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ export type Node =
|
||||||
| { k: "scale"; x: Node; mult: Decimal; label: string }
|
| { k: "scale"; x: Node; mult: Decimal; label: string }
|
||||||
| { k: "conv"; x: Node; target: ConvTarget }
|
| { k: "conv"; x: Node; target: ConvTarget }
|
||||||
| { k: "call"; name: string; args: Node[] }
|
| { k: "call"; name: string; args: Node[] }
|
||||||
| { k: "seq"; items: Node[] };
|
| { k: "seq"; items: Node[] }
|
||||||
|
| { k: "unknown" }
|
||||||
|
| { k: "goalseek"; lhs: Node; rhs: Node };
|
||||||
|
|
||||||
export interface ParsedLine {
|
export interface ParsedLine {
|
||||||
assign?: string;
|
assign?: string;
|
||||||
|
|
@ -73,6 +75,17 @@ export function parseLine(tokens: Token[], knownVars: Set<string>, line: string)
|
||||||
filtered.push(tk);
|
filtered.push(tk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Goal seek: `? * 1.2 = 1000` → find x where lhs(x) = rhs(x)
|
||||||
|
const hasUnknown = filtered.some((tk) => tk.t === "unknown");
|
||||||
|
if (hasUnknown) {
|
||||||
|
const assignIdx = filtered.findIndex((tk) => tk.t === "assign");
|
||||||
|
if (assignIdx >= 0) {
|
||||||
|
const lhs = new Parser(filtered.slice(0, assignIdx), knownVars).parseSeq();
|
||||||
|
const rhs = new Parser(filtered.slice(assignIdx + 1), knownVars).parseSeq();
|
||||||
|
if (lhs && rhs) return { expr: { k: "goalseek", lhs, rhs } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const p = new Parser(filtered, knownVars);
|
const p = new Parser(filtered, knownVars);
|
||||||
return { assign, expr: p.parseSeq() };
|
return { assign, expr: p.parseSeq() };
|
||||||
}
|
}
|
||||||
|
|
@ -419,6 +432,9 @@ class Parser {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case "unknown":
|
||||||
|
this.i++;
|
||||||
|
return { k: "unknown" };
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export type Token =
|
||||||
| { t: "semicolon"; start: number; end: number }
|
| { t: "semicolon"; start: number; end: number }
|
||||||
| { t: "bang"; start: number; end: number }
|
| { t: "bang"; start: number; end: number }
|
||||||
| { t: "word"; raw: string; start: number; end: number }
|
| { t: "word"; raw: string; start: number; end: number }
|
||||||
|
| { t: "unknown"; start: number; end: number }
|
||||||
| { t: "junk"; raw: string; start: number; end: number };
|
| { t: "junk"; raw: string; start: number; end: number };
|
||||||
|
|
||||||
const SYM_OPS: Record<string, Token["t"] | "plus" | "minus" | "mul" | "div" | "pow"> = {};
|
const SYM_OPS: Record<string, Token["t"] | "plus" | "minus" | "mul" | "div" | "pow"> = {};
|
||||||
|
|
@ -100,6 +101,7 @@ export function tokenize(line: string, reg: Registry): Token[] {
|
||||||
case "=": tokens.push({ t: "assign", ...span }); break;
|
case "=": tokens.push({ t: "assign", ...span }); break;
|
||||||
case ";": tokens.push({ t: "semicolon", ...span }); break;
|
case ";": tokens.push({ t: "semicolon", ...span }); break;
|
||||||
case "!": tokens.push({ t: "bang", ...span }); break;
|
case "!": tokens.push({ t: "bang", ...span }); break;
|
||||||
|
case "?": tokens.push({ t: "unknown", ...span }); break;
|
||||||
default: tokens.push({ t: "junk", raw: lx.raw, ...span });
|
default: tokens.push({ t: "junk", raw: lx.raw, ...span });
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue