Files
Aislo/A00_Common/spreadsheet/spreadsheet_address.ts
T
eomsangdonandClaude Opus 5.5 736dac4eb5 feat(spreadsheet): 산출근거 스프레드시트 A 엔진 핵심 — 주소 · 파서 · 풀이 · 의존 재계산 · 참조 옮김 · 명령 · 되돌리기
- 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
2026-09-27 20:14:31 +09:00

116 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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 };
}