/* ============================================================================= * spreadsheet_address.ts (주인 A) * 주소 — A1 ↔ 행열(0 부터) · 범위 글 · 열 글자 · 시트 이름 따옴표 · Map 열쇠. * 계약은 `spreadsheet_types.ts`. * ========================================================================== */ import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types"; import type { CellAddress, CellRange } from "./spreadsheet_types"; /** 0 → `A` · 27 → `AB` */ export function colName(c: number): string { let s = ""; for (let n = c + 1; n > 0; n = Math.floor((n - 1) / 26)) { s = String.fromCharCode(65 + ((n - 1) % 26)) + s; } return s; } /** `AB` → 27 · 못 읽으면 -1 (대소문자 가리지 않음) */ export function colIndex(letters: string): number { if (!/^[A-Za-z]{1,3}$/.test(letters)) return -1; let n = 0; for (const ch of letters.toUpperCase()) n = n * 26 + ch.charCodeAt(0) - 64; return n <= MAX_COLS ? n - 1 : -1; } /** 행 글(1 부터) → 0 부터 · 못 읽으면 -1 */ export function rowIndex(digits: string): number { const n = /^\d{1,7}$/.test(digits) ? Number(digits) : 0; return n >= 1 && n <= MAX_ROWS ? n - 1 : -1; } /** (30, 4) → `E31` */ export function toA1(r: number, c: number): string { return colName(c) + (r + 1); } /** `E31` · `$E$31` → {r:30, c:4} · 못 읽으면 null */ export function parseA1(text: string): CellAddress | null { const m = /^\$?([A-Za-z]{1,3})\$?(\d+)$/.exec(text.trim()); if (!m) return null; const c = colIndex(m[1]); const r = rowIndex(m[2]); return c < 0 || r < 0 ? null : { r, c }; } /** 칸 하나면 `E31` · 아니면 `C3:AD3` · 행 전체 `3:3` · 열 전체 `A:A` */ export function rangeToA1(range: CellRange): string { const { r0, c0, r1, c1 } = range; if (c0 === 0 && c1 === MAX_COLS - 1) return `${r0 + 1}:${r1 + 1}`; if (r0 === 0 && r1 === MAX_ROWS - 1) return `${colName(c0)}:${colName(c1)}`; const a = toA1(r0, c0); return r0 === r1 && c0 === c1 ? a : `${a}:${toA1(r1, c1)}`; } /** 닫힌 범위로 세움(뒤집힌 것 바로) */ export function normRange(r0: number, c0: number, r1: number, c1: number): CellRange { return { r0: Math.min(r0, r1), c0: Math.min(c0, c1), r1: Math.max(r0, r1), c1: Math.max(c0, c1), }; } /** `C3:AD3` · `E31` · `A:A` · `3:5` → 범위(뒤집힌 글은 바로 세움) · 못 읽으면 null */ export function parseRange(text: string): CellRange | null { const parts = text.trim().split(":"); if (parts.length > 2) return null; const [a, b = a] = parts; const p = parseA1(a); const q = parseA1(b); if (p && q) return normRange(p.r, p.c, q.r, q.c); if (parts.length !== 2) return null; const strip = (s: string) => s.replace(/^\$/, ""); const ca = colIndex(strip(a)); const cb = colIndex(strip(b)); if (ca >= 0 && cb >= 0) return normRange(0, ca, MAX_ROWS - 1, cb); const ra = rowIndex(strip(a)); const rb = rowIndex(strip(b)); if (ra >= 0 && rb >= 0) return normRange(ra, 0, rb, MAX_COLS - 1); return null; } export function inRange(range: CellRange, r: number, c: number): boolean { return r >= range.r0 && r <= range.r1 && c >= range.c0 && c <= range.c1; } export function rangesOverlap(a: CellRange, b: CellRange): boolean { return a.r0 <= b.r1 && b.r0 <= a.r1 && a.c0 <= b.c1 && b.c0 <= a.c1; } /** `a` 가 `b` 안에 통째로 듦 */ export function rangeInside(a: CellRange, b: CellRange): boolean { return a.r0 >= b.r0 && a.r1 <= b.r1 && a.c0 >= b.c0 && a.c1 <= b.c1; } /** 식에 넣을 시트 이름 — 글자 · 숫자 · _ 밖이 있으면 `'…'`(안의 `'` 는 `''`) */ export function quoteSheet(name: string): string { const plain = /^[\p{L}_][\p{L}\p{N}_]*$/u.test(name) && !/^[A-Za-z]{1,3}\d+$/.test(name) && // 칸 주소처럼 보이는 이름 !/^(TRUE|FALSE)$/i.test(name) && !/^R\d*C\d*$/i.test(name); // R1C1 처럼 보이는 이름 return plain ? name : `'${name.replace(/'/g, "''")}'`; } /** Map 열쇠 — r × MAX_COLS + c */ export function cellId(r: number, c: number): number { return r * MAX_COLS + c; } export function fromCellId(id: number): CellAddress { return { r: Math.floor(id / MAX_COLS), c: id % MAX_COLS }; }