From a9aaff54900e87daff2722d554dfeba597bab763 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:25:56 +0300 Subject: [PATCH] "time in Europe/Berlin" dropped the second word Two independent traps. The noise filter only keeps word tokens right after a conversion token (in/as/...); any other token type resets that, so the "/" in a slash-form IANA id ("Europe/Berlin") reset it before the parser ever saw "Berlin". The word-collecting loop in parseTarget() had the same blind spot even once the token was there. Both needed the same exception, or "Europe/Berlin" still degrades to just "Europe". --- src/engine/edge.test.ts | 3 +++ src/engine/parser.ts | 20 ++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/engine/edge.test.ts b/src/engine/edge.test.ts index 06c10b2..f2dcec3 100644 --- a/src/engine/edge.test.ts +++ b/src/engine/edge.test.ts @@ -232,4 +232,7 @@ describe("review fixes", () => { expect(calc("123456 in scientific")).toBe("1.23e5"); expect(calc("123456 in exponent")).toBe("1.23e5"); }); + it("timezone names with a slash resolve as IANA ids", () => { + expect(calc("time in Europe/Berlin")).toMatch(/^\d{2}:\d{2}$/); + }); }); diff --git a/src/engine/parser.ts b/src/engine/parser.ts index d0bded3..1f40105 100644 --- a/src/engine/parser.ts +++ b/src/engine/parser.ts @@ -72,6 +72,12 @@ export function parseLine(tokens: Token[], knownVars: Set, line: string) if (knownVars.has(tk.raw) || afterConv) filtered.push(tk); continue; } + // a bare "/" inside a timezone name ("Europe/Berlin") must not break the + // word-keeping chain, otherwise the second half gets filtered as noise + if (afterConv && tk.t === "op" && tk.op === "div") { + filtered.push(tk); + continue; + } afterConv = tk.t === "conv"; filtered.push(tk); } @@ -256,8 +262,18 @@ class Parser { } if (tk.t === "word") { const words: string[] = []; - while (this.peek()?.t === "word" && words.length < 4) { - words.push((this.next() as Token & { raw: string }).raw); + for (;;) { + const w = this.peek(); + if (w?.t === "word" && words.length < 4) { + words.push((this.next() as Token & { raw: string }).raw); + continue; + } + // "Europe/Berlin": a slash between timezone words is part of the name + if (w?.t === "op" && w.op === "div" && words.length > 0 && this.toks[this.i + 1]?.t === "word") { + this.next(); + continue; + } + break; } if (words.length > 0) return { type: "tz", words }; }