- address · parser — A1 ↔ 행열 · 낱말(편집기 색칠용 자리) · 엑셀 우선순위 나무(-2^2=4 · % · & · 비교) · 나무 → 글 - eval — 분수 풀이 · 오류 값 전파(왼쪽 먼저) · 형 바꾸기(빈 칸 · 수 글 · 참거짓) · 일반 형식 글 15 자리 · 무리수 15 자리 - graph — 의존 그래프 · 고친 칸에 딸린 칸만 재계산 · 순환 = 고리 칸마다 #CYCLE! + 까닭 - refshift — 행열 넣기 · 지우기 · 잘라 붙이기 · 시트 이름 · 시트 지우기 참조 옮김(지운 칸 #REF!) · 복사 상대 옮김 · 구글 R1C1 → A1 - commands · history · recalc — 명령마다 되돌림 짝 · 병합 규칙 · 서식 한 벌 표 · 묶음 원자 · Node 한 벌 풀이 - 시험 resources/tester/spreadsheet/test_spreadsheet_engine.py — 엑셀 규칙표 9 묶음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
/* =============================================================================
|
||
* 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 };
|
||
}
|