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.
This commit is contained in:
Horizontal42 2026-07-08 15:48:40 +03:00
parent f634b1858e
commit f2deaa1b94

View file

@ -34,25 +34,12 @@ function validDateMs(y: number, mo: number, d: number): number | null {
: null; : null;
} }
export function lexLine(text: string, dateOrder: DateOrder = "dmy"): Lex[] { function tryLexDate(rest: string, i: number, dateOrder: DateOrder): Lex | null {
const out: Lex[] = [];
let i = 0;
while (i < text.length) {
const ch = text[i];
if (ch === " " || ch === "\t" || ch === " ") {
i++;
continue;
}
const rest = text.slice(i);
if (ch >= "0" && ch <= "9") {
const iso = /^(\d{4})-(\d{2})-(\d{2})(?!\d)/.exec(rest); const iso = /^(\d{4})-(\d{2})-(\d{2})(?!\d)/.exec(rest);
if (iso) { if (iso) {
const ms = validDateMs(Number(iso[1]), Number(iso[2]), Number(iso[3])); const ms = validDateMs(Number(iso[1]), Number(iso[2]), Number(iso[3]));
if (ms !== null) { if (ms !== null) {
out.push({ type: "date", raw: iso[0], start: i, end: i + iso[0].length, dateMs: ms }); return { 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); const dm = SLASH_DOT_DATE_RE.exec(rest);
@ -64,33 +51,29 @@ export function lexLine(text: string, dateOrder: DateOrder = "dmy"): Lex[] {
const y = Number(dm[4]); const y = Number(dm[4]);
const ms = dayFirst ? validDateMs(y, b, a) : validDateMs(y, a, b); const ms = dayFirst ? validDateMs(y, b, a) : validDateMs(y, a, b);
if (ms !== null) { if (ms !== null) {
out.push({ type: "date", raw: dm[0], start: i, end: i + dm[0].length, dateMs: ms }); return { type: "date", raw: dm[0], start: i, end: i + dm[0].length, dateMs: ms };
i += dm[0].length;
continue;
} }
} }
return null;
} }
if (ch >= "0" && ch <= "9") {
function tryLexBaseNumber(rest: string, i: number): Lex | null {
const hex = /^0[xX][0-9a-fA-F]+/.exec(rest); const hex = /^0[xX][0-9a-fA-F]+/.exec(rest);
if (hex) { 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" }); return { 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); const bin = /^0[bB][01]+/.exec(rest);
if (bin) { 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" }); return { 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); const oct = /^0[oO][0-7]+/.exec(rest);
if (oct) { 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" }); return { 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;
} }
return null;
} }
if (ch >= "0" && ch <= "9") {
function tryLexDecimalNumber(rest: string, i: number): Lex | null {
let raw: string | null = null; let raw: string | null = null;
let cleaned = ""; let cleaned = "";
const sp = NUM_SPACE_GROUPED_RE.exec(rest); const sp = NUM_SPACE_GROUPED_RE.exec(rest);
@ -111,37 +94,65 @@ export function lexLine(text: string, dateOrder: DateOrder = "dmy"): Lex[] {
cleaned = raw; cleaned = raw;
} }
if (raw) { if (raw) {
out.push({ type: "num", raw, start: i, end: i + raw.length, value: new Decimal(cleaned), repr: "decimal" }); return { type: "num", raw, start: i, end: i + raw.length, value: new Decimal(cleaned), repr: "decimal" };
i += raw.length;
continue;
} }
} return null;
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 (ch === "@") { 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); const bracket = /^@\[([^\]]+)\]\.([\p{L}_][\p{L}\d_]*)/u.exec(rest);
if (bracket) { if (bracket) {
out.push({ type: "xref", raw: bracket[0], start: i, end: i + bracket[0].length, sheet: bracket[1].trim(), key: bracket[2] }); return { 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); const bare = /^@([\p{L}_][\p{L}\d_]*)\.([\p{L}_][\p{L}\d_]*)/u.exec(rest);
if (bare) { if (bare) {
out.push({ type: "xref", raw: bare[0], start: i, end: i + bare[0].length, sheet: bare[1], key: bare[2] }); return { type: "xref", raw: bare[0], start: i, end: i + bare[0].length, sheet: bare[1], key: bare[2] };
i += bare[0].length;
continue;
} }
return null;
} }
function tryLexWord(rest: string, i: number): Lex | null {
const word = WORD_RE.exec(rest); const word = WORD_RE.exec(rest);
if (word) { if (word) {
out.push({ type: "word", raw: word[0], start: i, end: i + word[0].length }); return { type: "word", raw: word[0], start: i, end: i + word[0].length };
i += word[0].length; }
return null;
}
export function lexLine(text: string, dateOrder: DateOrder = "dmy"): Lex[] {
const out: Lex[] = [];
let i = 0;
while (i < text.length) {
const ch = text[i];
if (ch === " " || ch === "\t" || ch === " ") {
i++;
continue;
}
const rest = text.slice(i);
let lex: Lex | null = null;
if (ch >= "0" && ch <= "9") {
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 (!lex) {
lex = tryLexWord(rest, i);
}
if (lex) {
out.push(lex);
i += lex.raw.length;
continue; continue;
} }