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
This commit is contained in:
@@ -1,33 +1,179 @@
|
||||
/* =============================================================================
|
||||
* spreadsheet_refshift.ts (주인 A)
|
||||
* 참조 옮김 — 행열 넣기 · 지우기 · 잘라 붙이기 · 시트 이름 바꾸기 때 식 글을 고쳐 적음(엑셀 규칙) ·
|
||||
* 복사 · 채우기 때 상대 참조만 옮김 · 구글 R1C1 식 → A1.
|
||||
* 참조 옮김 — 행열 넣기 · 지우기 · 잘라 붙이기 · 시트 이름 바꾸기 · 시트 지우기 때 식 글을 고쳐 적음
|
||||
* (엑셀 규칙) · 복사 · 채우기 때 상대 참조만 옮김 · 구글 R1C1 식 → A1.
|
||||
* 지운 칸 · 격자 밖으로 나간 참조는 `#REF!` · 범위는 남은 만큼 줄어듦(끝까지 지우면 `#REF!`).
|
||||
* 0 계약 머리 — 몸은 A 가 채움.
|
||||
* 낱말 단위로 참조만 바꿔 끼움 — 빈칸 · 괄호 · 수 글은 적힌 그대로 둠.
|
||||
*
|
||||
* 엑셀 규칙표:
|
||||
* 넣기 — 넣는 자리 이후를 가리키면 밀림 · 범위가 넣는 자리를 품으면 늘어남(첫 행 앞 넣기는 통째 밀림)
|
||||
* 지우기 — 지운 칸 하나 = `#REF!` · 범위는 남은 만큼 · 전부 지우면 `#REF!` · `$` 도 똑같이 옮김
|
||||
* 옮기기 — 옮긴 덩어리 안을 가리키던 참조(범위는 통째 들 때만)는 따라감 · 덮어쓴 자리를 가리키면 `#REF!`
|
||||
* 복사 — 상대만 (dr, dc) · 행 · 열 전체 범위는 그 축을 안 옮김
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CellAddress, Command, Workbook } from "./spreadsheet_types";
|
||||
import { quoteSheet, rangeInside } from "./spreadsheet_address";
|
||||
import { parseRef, renderRef, tokenize } from "./spreadsheet_parser";
|
||||
import type { RefParts } from "./spreadsheet_parser";
|
||||
import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types";
|
||||
import type { CellAddress, CellRange, Command, Workbook } from "./spreadsheet_types";
|
||||
|
||||
const todo = (): never => {
|
||||
throw new Error("spreadsheet_refshift: 아직 없음(A)");
|
||||
};
|
||||
const REF_ERR = "#REF!";
|
||||
|
||||
/** 행열 · 옮기기 · 시트이름 명령에 맞춰 식 글 고침(식이 든 시트 id = `host`) — 안 바뀌면 같은 글 */
|
||||
/** 참조 낱말만 `fn` 으로 바꿔 끼움(null = `#REF!`) */
|
||||
function mapRefs(formula: string, fn: (p: RefParts) => RefParts | string | null): string {
|
||||
let out = "";
|
||||
for (const t of tokenize(formula)) {
|
||||
const p = t.kind === "ref" ? parseRef(t.text) : null;
|
||||
if (!p) {
|
||||
out += t.text;
|
||||
continue;
|
||||
}
|
||||
const q = fn(p);
|
||||
out += q === null ? REF_ERR : typeof q === "string" ? q : renderRef(q);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 넣기 · 지우기 한 축 — [lo, hi] → 새 [lo, hi] · 다 지워지면 null */
|
||||
export function shiftSpan(
|
||||
lo: number,
|
||||
hi: number,
|
||||
insert: boolean,
|
||||
at: number,
|
||||
n: number,
|
||||
max: number,
|
||||
): [number, number] | null {
|
||||
if (insert) {
|
||||
if (lo >= at) [lo, hi] = [lo + n, hi + n];
|
||||
else if (hi >= at) hi += n;
|
||||
if (lo >= max) return null;
|
||||
return [lo, Math.min(hi, max - 1)];
|
||||
}
|
||||
const end = at + n - 1;
|
||||
if (lo >= at && hi <= end) return null;
|
||||
return [lo < at ? lo : lo > end ? lo - n : at, hi < at ? hi : hi > end ? hi - n : at - 1];
|
||||
}
|
||||
|
||||
const sheetIdOf = (book: Workbook, name: string) =>
|
||||
book.시트.find((s) => s.이름.toLowerCase() === name.toLowerCase())?.id ?? null;
|
||||
|
||||
const box = (p: RefParts): CellRange => ({
|
||||
r0: Math.min(p.a.r, p.b.r),
|
||||
c0: Math.min(p.a.c, p.b.c),
|
||||
r1: Math.max(p.a.r, p.b.r),
|
||||
c1: Math.max(p.a.c, p.b.c),
|
||||
});
|
||||
|
||||
/** 행열 · 옮기기 · 시트이름 · 시트지우기 명령에 맞춰 식 글 고침(식이 든 시트 id = `host`) — 안 바뀌면 같은 글.
|
||||
* `book` 은 명령 적용 **전** 문서(시트 이름으로 id 를 찾음). `newHost` = 옮기기로 식 칸 자체가 간 시트. */
|
||||
export function shiftForCommand(
|
||||
_formula: string,
|
||||
_host: string,
|
||||
_command: Command,
|
||||
_book: Workbook,
|
||||
formula: string,
|
||||
host: string,
|
||||
command: Command,
|
||||
book: Workbook,
|
||||
newHost: string = host,
|
||||
): string {
|
||||
return todo();
|
||||
if (command.종류 === "묶음")
|
||||
return command.명령.reduce((f, c) => shiftForCommand(f, host, c, book, newHost), formula);
|
||||
const target = (p: RefParts) => (p.sheet === null ? host : sheetIdOf(book, p.sheet));
|
||||
const nameOf = (id: string) => book.시트.find((s) => s.id === id)?.이름 ?? "";
|
||||
|
||||
switch (command.종류) {
|
||||
case "행넣기":
|
||||
case "행지우기":
|
||||
case "열넣기":
|
||||
case "열지우기": {
|
||||
const rows = command.종류.startsWith("행");
|
||||
const insert = command.종류.endsWith("넣기");
|
||||
return mapRefs(formula, (p) => {
|
||||
if (target(p) !== command.시트) return p;
|
||||
if (rows ? p.kind === "cols" : p.kind === "rows") return p; // 다른 축 전체 범위
|
||||
const [lo, hi] = rows
|
||||
? [p.a.r, p.b.r].sort((x, y) => x - y)
|
||||
: [p.a.c, p.b.c].sort((x, y) => x - y);
|
||||
const span = shiftSpan(lo, hi, insert, command.at, command.수, rows ? MAX_ROWS : MAX_COLS);
|
||||
if (!span) return null;
|
||||
const q = structuredClone(p);
|
||||
// 적힌 차례(뒤집힌 범위)는 바로 세움 — 엑셀도 다시 적을 때 바로 세움
|
||||
if (rows) [q.a.r, q.b.r] = span;
|
||||
else [q.a.c, q.b.c] = span;
|
||||
return q;
|
||||
});
|
||||
}
|
||||
case "옮기기": {
|
||||
const src = command.범위;
|
||||
const dr = command.r - src.r0;
|
||||
const dc = command.c - src.c0;
|
||||
const dst: CellRange = { r0: command.r, c0: command.c, r1: src.r1 + dr, c1: src.c1 + dc };
|
||||
return mapRefs(formula, (p) => {
|
||||
const t = target(p);
|
||||
if (t === null) return p;
|
||||
const b = box(p);
|
||||
let to = t;
|
||||
const q = structuredClone(p);
|
||||
if (t === command.시트 && p.kind !== "cols" && p.kind !== "rows" && rangeInside(b, src)) {
|
||||
to = command.대상시트;
|
||||
for (const pt of [q.a, q.b]) {
|
||||
pt.r += dr;
|
||||
pt.c += dc;
|
||||
}
|
||||
} else if (t === command.대상시트 && rangeInside(b, dst)) return null;
|
||||
if (to === t && newHost === host) return q;
|
||||
q.prefix = to === newHost ? "" : `${quoteSheet(nameOf(to))}!`;
|
||||
return q;
|
||||
});
|
||||
}
|
||||
case "시트이름":
|
||||
return mapRefs(formula, (p) =>
|
||||
p.sheet !== null && target(p) === command.시트
|
||||
? { ...p, prefix: `${quoteSheet(command.이름)}!` }
|
||||
: p,
|
||||
);
|
||||
case "시트지우기":
|
||||
return mapRefs(formula, (p) => (p.sheet !== null && target(p) === command.시트 ? null : p));
|
||||
default:
|
||||
return formula;
|
||||
}
|
||||
}
|
||||
|
||||
/** 복사 · 채우기 — 상대 참조만 (dr, dc) 옮김 · 격자 밖은 `#REF!` */
|
||||
export function moveFormula(_formula: string, _dr: number, _dc: number): string {
|
||||
return todo();
|
||||
export function moveFormula(formula: string, dr: number, dc: number): string {
|
||||
return mapRefs(formula, (p) => {
|
||||
const q = structuredClone(p);
|
||||
for (const pt of [q.a, q.b]) {
|
||||
if (!pt.absR && p.kind !== "cols") pt.r += dr;
|
||||
if (!pt.absC && p.kind !== "rows") pt.c += dc;
|
||||
if (pt.r < 0 || pt.r >= MAX_ROWS || pt.c < 0 || pt.c >= MAX_COLS) return null;
|
||||
}
|
||||
return q;
|
||||
});
|
||||
}
|
||||
|
||||
/** 구글 `data-sheets-formula`(`=2*R[0]C[-1]`) → 붙일 자리 기준 A1 식(앞 `=` 뺌) */
|
||||
export function r1c1ToA1(_formula: string, _at: CellAddress): string {
|
||||
return todo();
|
||||
export function r1c1ToA1(formula: string, at: CellAddress): string {
|
||||
const text = formula.startsWith("=") ? formula.slice(1) : formula;
|
||||
const axis = (part: string | undefined, base: number): [number, boolean] =>
|
||||
part === undefined || part === ""
|
||||
? [base, false]
|
||||
: part.startsWith("[")
|
||||
? [base + Number(part.slice(1, -1)), false]
|
||||
: [Number(part) - 1, true];
|
||||
// 글 · 따옴표 시트 이름은 건너뜀 · 이름 한가운데(`ROUND`)는 안 건드림
|
||||
return text.replace(
|
||||
/("(?:[^"]|"")*"|'(?:[^']|'')*')|(?<![\p{L}\p{N}_.$])R(\[-?\d+\]|\d+)?C(\[-?\d+\]|\d+)?(?![\p{L}\p{N}_.(])/gu,
|
||||
(whole, quoted: string | undefined, rp: string | undefined, cp: string | undefined) => {
|
||||
if (quoted) return whole;
|
||||
const [r, absR] = axis(rp, at.r);
|
||||
const [c, absC] = axis(cp, at.c);
|
||||
if (r < 0 || r >= MAX_ROWS || c < 0 || c >= MAX_COLS) return REF_ERR;
|
||||
return renderRef({
|
||||
prefix: "",
|
||||
sheet: null,
|
||||
kind: "cell",
|
||||
a: { r, c, absR, absC },
|
||||
b: { r, c, absR, absC },
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user