fix(spreadsheet): 이름 정의 풀이 — =SUM(값들) 이 범위로 풀림 · 행열 넣기 · 지우기 때 이름 범위 따라감
- 엔진: 식 나무의 이름을 Sheet.이름정의 범위로 바꿔 풂(자기 시트 먼저 · 없으면 다른 시트 · 없는 이름 #NAME?) · 의존 그래프도 범위로 이어짐 - 식 캐시는 시트마다 · rebuild 때 비움 — 이름 바꾸기 · 지우기(시트통째) 뒤 새로 풂 - 행열 넣기 · 지우기: 병합과 같은 범위 옮김으로 이름 범위도 옮김 · 통째로 지워진 이름은 뺌 - 수식 계산 걸음도 이름을 풂 - 시험: test_spreadsheet_analysis.py names 묶음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF
This commit is contained in:
@@ -292,16 +292,24 @@ function insertDelete(
|
||||
setComments(s, notes);
|
||||
}
|
||||
|
||||
const merges: CellRange[] = [];
|
||||
for (const m of mergesOf(s)) {
|
||||
const shiftRange = (m: CellRange) => {
|
||||
const sp = rows
|
||||
? shiftSpan(m.r0, m.r1, insert, at, n, max)
|
||||
: shiftSpan(m.c0, m.c1, insert, at, n, max);
|
||||
if (!sp) continue;
|
||||
const next = rows ? { ...m, r0: sp[0], r1: sp[1] } : { ...m, c0: sp[0], c1: sp[1] };
|
||||
if (next.r0 !== next.r1 || next.c0 !== next.c1) merges.push(next);
|
||||
return sp && (rows ? { ...m, r0: sp[0], r1: sp[1] } : { ...m, c0: sp[0], c1: sp[1] });
|
||||
};
|
||||
const merges: CellRange[] = [];
|
||||
for (const m of mergesOf(s)) {
|
||||
const next = shiftRange(m);
|
||||
if (next && (next.r0 !== next.r1 || next.c0 !== next.c1)) merges.push(next);
|
||||
}
|
||||
setMerges(s, merges);
|
||||
// 이름 범위도 따라감 · 범위가 통째로 지워진 이름은 뺌(엑셀은 `#REF!` 이름으로 남김 — 여기선 식이 `#NAME?`)
|
||||
if (s.이름정의)
|
||||
s.이름정의 = s.이름정의.flatMap((d) => {
|
||||
const 범위 = shiftRange(d.범위);
|
||||
return 범위 ? [{ ...d, 범위 }] : [];
|
||||
});
|
||||
|
||||
const axis = rows ? "행" : "열";
|
||||
const table = s[axis];
|
||||
|
||||
@@ -15,7 +15,7 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { parseA1, quoteSheet, toA1 } from "./spreadsheet_address";
|
||||
import { isError, isRange, numberText } from "./spreadsheet_eval";
|
||||
import type { PlacedRange } from "./spreadsheet_eval";
|
||||
import { evalSafe } from "./spreadsheet_graph";
|
||||
import { bindNames, evalSafe } from "./spreadsheet_graph";
|
||||
import { formulaToText, parseFormula } from "./spreadsheet_parser";
|
||||
import type {
|
||||
CalcEngine,
|
||||
@@ -157,7 +157,7 @@ export function evalSteps(
|
||||
for (const n of nodes) {
|
||||
if (n === p.node) vals.set(n, engine.value(sheet, r, c));
|
||||
else {
|
||||
const v = evalSafe(n, ctx);
|
||||
const v = evalSafe(bindNames(n, book, sheet), ctx);
|
||||
if (isRange(v)) continue; // 범위 값(INDEX 등)은 글 그대로
|
||||
vals.set(n, v);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
FormulaNode,
|
||||
ParseResult,
|
||||
Scalar,
|
||||
Sheet,
|
||||
SheetCellAddress,
|
||||
Workbook,
|
||||
} from "./spreadsheet_types";
|
||||
@@ -82,7 +83,36 @@ function collectRefs(
|
||||
}
|
||||
}
|
||||
|
||||
/** 나무 하나 풂 — 던진 오류는 오류 값으로(0 나눔 → `#DIV/0!` · 나머지 `#VALUE!`) */
|
||||
/** 이름(`값들`) → 정의한 범위 참조(자기 시트 이름 먼저 · 없으면 다른 시트) · 없는 이름은 그대로(→ `#NAME?`) */
|
||||
export function bindNames(node: FormulaNode, book: Workbook, host: string): FormulaNode {
|
||||
const go = (n: FormulaNode): FormulaNode => {
|
||||
switch (n.type) {
|
||||
case "name": {
|
||||
const low = n.name.toLowerCase();
|
||||
const def = (s?: Sheet) => s?.이름정의?.find((d) => d.이름.toLowerCase() === low);
|
||||
const s = [book.시트.find((x) => x.id === host), ...book.시트].find(def);
|
||||
if (!s) return n;
|
||||
const g = def(s)!.범위;
|
||||
const sheet = s.id === host ? null : s.이름;
|
||||
const from = { r: g.r0, c: g.c0, absR: true, absC: true };
|
||||
const to = { r: g.r1, c: g.c1, absR: true, absC: true };
|
||||
return g.r0 === g.r1 && g.c0 === g.c1
|
||||
? { type: "cell", sheet, ref: from }
|
||||
: { type: "range", sheet, from, to };
|
||||
}
|
||||
case "unary":
|
||||
return { ...n, arg: go(n.arg) };
|
||||
case "binary":
|
||||
return { ...n, left: go(n.left), right: go(n.right) };
|
||||
case "call":
|
||||
return { ...n, args: n.args.map(go) };
|
||||
}
|
||||
return n;
|
||||
};
|
||||
return book.시트.some((s) => s.이름정의?.length) ? go(node) : node;
|
||||
}
|
||||
|
||||
/** 나무 하나 풂 —던진 오류는 오류 값으로(0 나눔 → `#DIV/0!` · 나머지 `#VALUE!`) */
|
||||
export function evalSafe(node: FormulaNode, ctx: EvalContext): EvalResult {
|
||||
try {
|
||||
return evaluate(node, ctx);
|
||||
@@ -111,9 +141,15 @@ export function createCalcEngine(book: Workbook): CalcEngine {
|
||||
|
||||
const sheetId = (name: string) => names.get(name.toLowerCase()) ?? null;
|
||||
|
||||
const parse = (text: string) => {
|
||||
let p = parsed.get(text);
|
||||
if (!p) parsed.set(text, (p = parseFormula(text)));
|
||||
/** 식 나무(이름은 범위로 바꿈) — 시트마다 캐시 · rebuild 때 비움 */
|
||||
const parse = (text: string, host: string) => {
|
||||
const k = `${host}!${text}`;
|
||||
let p = parsed.get(k);
|
||||
if (!p) {
|
||||
p = parseFormula(text);
|
||||
if (p.ok) p.node = bindNames(p.node, book, host);
|
||||
parsed.set(k, p);
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
@@ -145,7 +181,7 @@ export function createCalcEngine(book: Workbook): CalcEngine {
|
||||
vals.set(key, inputValue(cell.값!));
|
||||
return;
|
||||
}
|
||||
const p = parse(cell.식);
|
||||
const p = parse(cell.식, sheet.id);
|
||||
const deps: Dep[] = [];
|
||||
if (p.ok) collectRefs(p.node, sheet.id, sheetId, deps);
|
||||
precs.set(key, deps);
|
||||
@@ -167,6 +203,7 @@ export function createCalcEngine(book: Workbook): CalcEngine {
|
||||
|
||||
function rebuild(next: Workbook) {
|
||||
book = next;
|
||||
parsed.clear();
|
||||
sheets = new Map();
|
||||
names = new Map();
|
||||
precs = new Map();
|
||||
@@ -294,7 +331,7 @@ export function createCalcEngine(book: Workbook): CalcEngine {
|
||||
}
|
||||
|
||||
function run(text: string, ctx: EvalContext): Scalar {
|
||||
const p = parse(text);
|
||||
const p = parse(text, ctx.sheet);
|
||||
if (!p.ok) return err("#NAME?", `식을 못 읽음: ${p.message} (${p.at + 1}번째 글자)`);
|
||||
const one = intersect(evalSafe(p.node, ctx), ctx);
|
||||
return one === null ? ZERO : one;
|
||||
|
||||
Reference in New Issue
Block a user