From 881fb4809afb14e12288fa8470853dfa731bc604 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:02:58 +0300 Subject: [PATCH] stop '5 min' from losing the minute unit to the min() aggregate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit registerCoreVocab() registers "min" as the aggregate function name before registerUnits() registers it as the minute abbreviation, and first-registration-wins means the unit phrase never lands. "5 min" silently became either plain "5" or got overwritten by a MIN of the preceding block. Disambiguate right after tokenizing: "min"/"мин" immediately after a number can only mean minutes. --- src/engine/edge.test.ts | 11 +++++++++++ src/engine/tokenizer.ts | 20 +++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/engine/edge.test.ts b/src/engine/edge.test.ts index f97dd07..b4fc63b 100644 --- a/src/engine/edge.test.ts +++ b/src/engine/edge.test.ts @@ -291,4 +291,15 @@ describe("regression 2026-08-12", () => { expect(r[1]).toBeNull(); expect(r[2]).toBe("4"); }); + + it("'5 min' means 5 minutes, not the min() aggregate", () => { + expect(calc("5 min")).toBe("5 min"); + expect(calc("5 мин")).toBe("5 min"); + }); + it("standalone 'min'/'max' aggregate lines still work", () => { + const r = eng.evaluateDocument("10\n20\n5\nmin").map((x) => x.text); + expect(r).toEqual(["10", "20", "5", "5"]); + const r2 = eng.evaluateDocument("10\n20\n5\nmax").map((x) => x.text); + expect(r2).toEqual(["10", "20", "5", "20"]); + }); }); diff --git a/src/engine/tokenizer.ts b/src/engine/tokenizer.ts index a2e1391..06b2cf6 100644 --- a/src/engine/tokenizer.ts +++ b/src/engine/tokenizer.ts @@ -115,7 +115,7 @@ export function tokenize(line: string, reg: Registry, dateOrder: DateOrder = "dm tokens.push({ t: "word", raw: lx.raw, ...span }); i++; } - return disambiguateIn(tokens, reg, line); + return disambiguateMinAgg(disambiguateIn(tokens, reg, line), reg, line); } /** @@ -138,3 +138,21 @@ function disambiguateIn(tokens: Token[], reg: Registry, line: string): Token[] { return tk; }); } + +/** + * "5 min": the "min" aggregate keyword (registered first, so it wins the + * phrase in the registry) collides with the minute unit's "min"/"мин" + * abbreviation. Right after a number it can only mean minutes — the + * standalone `min` aggregate line has no preceding number to collide with. + */ +function disambiguateMinAgg(tokens: Token[], reg: Registry, line: string): Token[] { + const minute = reg.unitsById.get("minute"); + if (!minute) return tokens; + return tokens.map((tk, i) => { + if (tk.t !== "agg" || tk.name !== "min") return tk; + const raw = line.slice(tk.start, tk.end).toLowerCase(); + if (raw !== "min" && raw !== "мин") return tk; + if (tokens[i - 1]?.t !== "num") return tk; + return { t: "unit", unit: minute, start: tk.start, end: tk.end } as Token; + }); +}