/**
* 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 ``/`