From f2deaa1b946ba2e0dba62cb569f823c50edac034 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:48:40 +0300 Subject: [PATCH] split lexLine's number/date matching into named helpers lexLine had grown into one long function trying dates, hex/bin/oct, grouped numbers, decimals and xrefs inline. Extracted each into a tryLex* helper with the same fallback order, so the driving loop reads as a priority list instead of nested ifs. --- src/engine/lexer.ts | 197 +++++++++++++++++++++++--------------------- 1 file changed, 104 insertions(+), 93 deletions(-) diff --git a/src/engine/lexer.ts b/src/engine/lexer.ts index d06a32c..f65f79d 100644 --- a/src/engine/lexer.ts +++ b/src/engine/lexer.ts @@ -34,6 +34,99 @@ function validDateMs(y: number, mo: number, d: number): number | null { : null; } +function tryLexDate(rest: string, i: number, dateOrder: DateOrder): Lex | null { + const iso = /^(\d{4})-(\d{2})-(\d{2})(?!\d)/.exec(rest); + if (iso) { + const ms = validDateMs(Number(iso[1]), Number(iso[2]), Number(iso[3])); + if (ms !== null) { + return { type: "date", raw: iso[0], start: i, end: i + iso[0].length, dateMs: ms }; + } + } + const dm = SLASH_DOT_DATE_RE.exec(rest); + if (dm) { + const a = Number(dm[1]); + const b = Number(dm[3]); + // a>12 must be the day, b>12 must be the month; both ≤12 → configured order + const dayFirst = a > 12 ? true : b > 12 ? false : dateOrder === "dmy"; + const y = Number(dm[4]); + const ms = dayFirst ? validDateMs(y, b, a) : validDateMs(y, a, b); + if (ms !== null) { + return { type: "date", raw: dm[0], start: i, end: i + dm[0].length, dateMs: ms }; + } + } + return null; +} + +function tryLexBaseNumber(rest: string, i: number): Lex | null { + const hex = /^0[xX][0-9a-fA-F]+/.exec(rest); + if (hex) { + return { type: "num", raw: hex[0], start: i, end: i + hex[0].length, value: new Decimal(parseInt(hex[0], 16)), repr: "hex" }; + } + const bin = /^0[bB][01]+/.exec(rest); + if (bin) { + return { type: "num", raw: bin[0], start: i, end: i + bin[0].length, value: new Decimal(parseInt(bin[0].slice(2), 2)), repr: "binary" }; + } + const oct = /^0[oO][0-7]+/.exec(rest); + if (oct) { + return { type: "num", raw: oct[0], start: i, end: i + oct[0].length, value: new Decimal(parseInt(oct[0].slice(2), 8)), repr: "octal" }; + } + return null; +} + +function tryLexDecimalNumber(rest: string, i: number): Lex | null { + let raw: string | null = null; + let cleaned = ""; + const sp = NUM_SPACE_GROUPED_RE.exec(rest); + const cg = sp ? null : NUM_COMMA_GROUPED_RE.exec(rest); + const dc = sp || cg ? null : NUM_DECIMAL_COMMA_RE.exec(rest); + const pl = sp || cg || dc ? null : NUM_PLAIN_RE.exec(rest); + if (sp) { + raw = sp[0]; + cleaned = raw.replace(/[   ]/g, "").replace(",", "."); + } else if (cg) { + raw = cg[0]; + cleaned = raw.replace(/,/g, ""); + } else if (dc) { + raw = dc[0]; + cleaned = raw.replace(",", "."); + } else if (pl) { + raw = pl[0]; + cleaned = raw; + } + if (raw) { + return { type: "num", raw, start: i, end: i + raw.length, value: new Decimal(cleaned), repr: "decimal" }; + } + return null; +} + +function tryLexDotNumber(rest: string, i: number): Lex | null { + if (/^\.\d/.test(rest)) { + const m = /^\.\d+/.exec(rest)!; + return { type: "num", raw: m[0], start: i, end: i + m[0].length, value: new Decimal(m[0]), repr: "decimal" }; + } + return null; +} + +function tryLexXRef(rest: string, i: number): Lex | null { + const bracket = /^@\[([^\]]+)\]\.([\p{L}_][\p{L}\d_]*)/u.exec(rest); + if (bracket) { + return { type: "xref", raw: bracket[0], start: i, end: i + bracket[0].length, sheet: bracket[1].trim(), key: bracket[2] }; + } + const bare = /^@([\p{L}_][\p{L}\d_]*)\.([\p{L}_][\p{L}\d_]*)/u.exec(rest); + if (bare) { + return { type: "xref", raw: bare[0], start: i, end: i + bare[0].length, sheet: bare[1], key: bare[2] }; + } + return null; +} + +function tryLexWord(rest: string, i: number): Lex | null { + const word = WORD_RE.exec(rest); + if (word) { + return { type: "word", raw: word[0], start: i, end: i + word[0].length }; + } + return null; +} + export function lexLine(text: string, dateOrder: DateOrder = "dmy"): Lex[] { const out: Lex[] = []; let i = 0; @@ -45,103 +138,21 @@ export function lexLine(text: string, dateOrder: DateOrder = "dmy"): Lex[] { } const rest = text.slice(i); + let lex: Lex | null = null; if (ch >= "0" && ch <= "9") { - const iso = /^(\d{4})-(\d{2})-(\d{2})(?!\d)/.exec(rest); - if (iso) { - const ms = validDateMs(Number(iso[1]), Number(iso[2]), Number(iso[3])); - if (ms !== null) { - out.push({ type: "date", raw: iso[0], start: i, end: i + iso[0].length, dateMs: ms }); - i += iso[0].length; - continue; - } - } - const dm = SLASH_DOT_DATE_RE.exec(rest); - if (dm) { - const a = Number(dm[1]); - const b = Number(dm[3]); - // a>12 must be the day, b>12 must be the month; both ≤12 → configured order - const dayFirst = a > 12 ? true : b > 12 ? false : dateOrder === "dmy"; - const y = Number(dm[4]); - const ms = dayFirst ? validDateMs(y, b, a) : validDateMs(y, a, b); - if (ms !== null) { - out.push({ type: "date", raw: dm[0], start: i, end: i + dm[0].length, dateMs: ms }); - i += dm[0].length; - continue; - } - } + lex = tryLexDate(rest, i, dateOrder) || tryLexBaseNumber(rest, i) || tryLexDecimalNumber(rest, i); + } else if (ch === ".") { + lex = tryLexDotNumber(rest, i); + } else if (ch === "@") { + lex = tryLexXRef(rest, i); } - if (ch >= "0" && ch <= "9") { - const hex = /^0[xX][0-9a-fA-F]+/.exec(rest); - if (hex) { - out.push({ type: "num", raw: hex[0], start: i, end: i + hex[0].length, value: new Decimal(parseInt(hex[0], 16)), repr: "hex" }); - i += hex[0].length; - continue; - } - const bin = /^0[bB][01]+/.exec(rest); - if (bin) { - out.push({ type: "num", raw: bin[0], start: i, end: i + bin[0].length, value: new Decimal(parseInt(bin[0].slice(2), 2)), repr: "binary" }); - i += bin[0].length; - continue; - } - const oct = /^0[oO][0-7]+/.exec(rest); - if (oct) { - out.push({ type: "num", raw: oct[0], start: i, end: i + oct[0].length, value: new Decimal(parseInt(oct[0].slice(2), 8)), repr: "octal" }); - i += oct[0].length; - continue; - } - } - if (ch >= "0" && ch <= "9") { - let raw: string | null = null; - let cleaned = ""; - const sp = NUM_SPACE_GROUPED_RE.exec(rest); - const cg = sp ? null : NUM_COMMA_GROUPED_RE.exec(rest); - const dc = sp || cg ? null : NUM_DECIMAL_COMMA_RE.exec(rest); - const pl = sp || cg || dc ? null : NUM_PLAIN_RE.exec(rest); - if (sp) { - raw = sp[0]; - cleaned = raw.replace(/[   ]/g, "").replace(",", "."); - } else if (cg) { - raw = cg[0]; - cleaned = raw.replace(/,/g, ""); - } else if (dc) { - raw = dc[0]; - cleaned = raw.replace(",", "."); - } else if (pl) { - raw = pl[0]; - cleaned = raw; - } - if (raw) { - out.push({ type: "num", raw, start: i, end: i + raw.length, value: new Decimal(cleaned), repr: "decimal" }); - i += raw.length; - continue; - } - } - if (ch === "." && /^\.\d/.test(rest)) { - const m = /^\.\d+/.exec(rest)!; - out.push({ type: "num", raw: m[0], start: i, end: i + m[0].length, value: new Decimal(m[0]), repr: "decimal" }); - i += m[0].length; - continue; + if (!lex) { + lex = tryLexWord(rest, i); } - if (ch === "@") { - const bracket = /^@\[([^\]]+)\]\.([\p{L}_][\p{L}\d_]*)/u.exec(rest); - if (bracket) { - out.push({ type: "xref", raw: bracket[0], start: i, end: i + bracket[0].length, sheet: bracket[1].trim(), key: bracket[2] }); - i += bracket[0].length; - continue; - } - const bare = /^@([\p{L}_][\p{L}\d_]*)\.([\p{L}_][\p{L}\d_]*)/u.exec(rest); - if (bare) { - out.push({ type: "xref", raw: bare[0], start: i, end: i + bare[0].length, sheet: bare[1], key: bare[2] }); - i += bare[0].length; - continue; - } - } - - const word = WORD_RE.exec(rest); - if (word) { - out.push({ type: "word", raw: word[0], start: i, end: i + word[0].length }); - i += word[0].length; + if (lex) { + out.push(lex); + i += lex.raw.length; continue; }