random() with no arguments threw before reaching its own branch

evalCall checked args[0] and threw "needs an argument" before ever
falling into the random case, which is the one built-in that's
valid with zero args. Dispatch it first.
This commit is contained in:
Horizontal42 2026-07-03 11:21:24 +03:00
parent 01697daaac
commit df7761401b
2 changed files with 25 additions and 10 deletions

View file

@ -207,3 +207,16 @@ describe("regressions", () => {
expect(v?.kind).toBe("date"); expect(v?.kind).toBe("date");
}); });
}); });
describe("review fixes", () => {
it("random() with no arguments returns a value in [0,1]", () => {
const r = calc("random()");
expect(r).not.toBeNull();
const v = Number(r);
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
});
it("random() participates in arithmetic", () => {
expect(calc("random() + 1")).not.toBeNull();
});
});

View file

@ -529,6 +529,7 @@ function evalAgg(name: "sum" | "avg" | "prev" | "count" | "min" | "max" | "produ
function evalCall(name: string, args: Value[], ctx: EvalCtx): Value { function evalCall(name: string, args: Value[], ctx: EvalCtx): Value {
const custom = ctx.reg.customFuncs.get(name); const custom = ctx.reg.customFuncs.get(name);
if (custom) return custom(args); if (custom) return custom(args);
if (name === "random") return evalRandom(args, ctx); // random() is valid with no args
const x = args[0]; const x = args[0];
if (!x) throw new EvalError(`${name} needs an argument`); if (!x) throw new EvalError(`${name} needs an argument`);
@ -570,16 +571,6 @@ function evalCall(name: string, args: Value[], ctx: EvalCtx): Value {
case "ceil": return x.kind === "quantity" ? { ...x, value: x.value.ceil() } : qty(n().ceil()); case "ceil": return x.kind === "quantity" ? { ...x, value: x.value.ceil() } : qty(n().ceil());
case "floor": return x.kind === "quantity" ? { ...x, value: x.value.floor() } : qty(n().floor()); case "floor": return x.kind === "quantity" ? { ...x, value: x.value.floor() } : qty(n().floor());
case "fact": case "factorial": return qty(factorial(n())); case "fact": case "factorial": return qty(factorial(n()));
case "random": {
const seed = strHash(ctx.line.lineText) ^ (ctx.line.index * 0x9e3779b9);
const rand = mulberry32(seed);
if (args.length === 0) return qty(new Decimal(rand));
const toNum = (v: Value) => v.kind === "quantity" ? v.value.toNumber() : 0;
const [lo, hi] = args.length === 1 ? [0, toNum(args[0]!)] : [toNum(args[0]!), toNum(args[1]!)];
const isInt = Number.isInteger(lo) && Number.isInteger(hi);
const raw = isInt ? Math.floor(rand * (hi - lo + 1)) + lo : rand * (hi - lo) + lo;
return qty(new Decimal(raw));
}
case "until": { case "until": {
if (x.kind !== "date") throw new EvalError("until needs a date"); if (x.kind !== "date") throw new EvalError("until needs a date");
return evalDateArith("minus", x, { kind: "date", ms: startOfToday(), hasTime: false }); return evalDateArith("minus", x, { kind: "date", ms: startOfToday(), hasTime: false });
@ -592,6 +583,17 @@ function evalCall(name: string, args: Value[], ctx: EvalCtx): Value {
} }
} }
function evalRandom(args: Value[], ctx: EvalCtx): Value {
const seed = strHash(ctx.line.lineText) ^ (ctx.line.index * 0x9e3779b9);
const rand = mulberry32(seed);
if (args.length === 0) return qty(new Decimal(rand));
const toNum = (v: Value) => v.kind === "quantity" ? v.value.toNumber() : 0;
const [lo, hi] = args.length === 1 ? [0, toNum(args[0]!)] : [toNum(args[0]!), toNum(args[1]!)];
const isInt = Number.isInteger(lo) && Number.isInteger(hi);
const raw = isInt ? Math.floor(rand * (hi - lo + 1)) + lo : rand * (hi - lo) + lo;
return qty(new Decimal(raw));
}
function strHash(s: string): number { function strHash(s: string): number {
let h = 0x811c9dc5; let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) { for (let i = 0; i < s.length; i++) {