stop '5 min' from losing the minute unit to the min() aggregate

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.
This commit is contained in:
Horizontal42 2026-08-12 20:02:58 +03:00
parent 741ea16e40
commit 881fb4809a
2 changed files with 30 additions and 1 deletions

View file

@ -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"]);
});
});

View file

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