/** * Reference helpers: A1 <-> (col,row) conversion and range expansion. * * All coordinates the engine uses internally are 0-based. The A1 notation * exposed to the user is 1-based for rows and letter-based for columns * (A=0, B=1, ..., Z=25, AA=26, ...). */ import { tokenize, type Token } from './lexer'; const CELL_RE = /^(\$?)([A-Za-z]{1,3})(\$?)([0-9]{1,7})$/; /** "A" -> 0, "B" -> 1, "Z" -> 25, "AA" -> 26 ... */ export function colToNumber(letters: string): number { let n = 0; const up = letters.toUpperCase(); for (let i = 0; i < up.length; i++) { n = n * 26 + (up.charCodeAt(i) - 64); // 'A' -> 1 } return n - 1; } /** 0 -> "A", 25 -> "Z", 26 -> "AA" ... */ export function numberToCol(n: number): string { let s = ''; let x = n + 1; while (x > 0) { const rem = (x - 1) % 26; s = String.fromCharCode(65 + rem) + s; x = Math.floor((x - 1) / 26); } return s; } /** True if `text` looks like a bare cell reference (e.g. "A1", "$B$2", "aa10"). */ export function looksLikeCell(text: string): boolean { return CELL_RE.test(text); } /** * Parse "A1" / "$A1" / "A$1" / "$A$1" -> 0-based coords plus which axes are * `$`-locked (absolute). A locked axis is exempt from the reference shift a * copy/fill applies (see `shiftFormula` in the store); an unlocked one is * "relative" and always shifts with it. Throws for malformed input. */ export function parseA1( text: string, ): { col: number; row: number; colAbs: boolean; rowAbs: boolean } { const m = CELL_RE.exec(text); if (!m) throw new Error(`Bad cell reference: ${text}`); return { col: colToNumber(m[2]), row: Number(m[4]) - 1, colAbs: m[1] === '$', rowAbs: m[3] === '$' }; } /** Build "A1" from 0-based coordinates; `colAbs`/`rowAbs` add the `$` lock markers. */ export function toA1(col: number, row: number, colAbs = false, rowAbs = false): string { return `${colAbs ? '$' : ''}${numberToCol(col)}${rowAbs ? '$' : ''}${row + 1}`; } /** * Excel/Numbers-style F4: cycle every cell reference through * relative -> absolute -> row-locked -> column-locked -> relative, each * from its own current state. * * With a collapsed selection (`selStart === selEnd`, the common case -- just * a caret), this toggles the single reference at/adjacent to it. With a real * text selection, it toggles *every* reference the selection overlaps at * once -- select a stretch of formula text spanning several references and * F4 adjusts them all in one press, same as Excel. * * `selStart`/`selEnd` are 0-based offsets into `formula` (which must start * with "="), i.e. straight from an ``/`