- 엔진: 식 나무의 이름을 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
233 lines
9.2 KiB
JavaScript
233 lines
9.2 KiB
JavaScript
/* 수식 분석 엔진 시험 헬퍼 — 추적 화살표 단계 · 수식 계산 걸음 · 오류 검사 까닭을 묶음마다 돌려 틀린 것 목록을 JSON 으로.
|
|
* 적재 · 가짜 함수는 `helper_spreadsheet_engine.cjs` 것을 씀.
|
|
*/
|
|
const { A, G, load } = require("./helper_spreadsheet_engine.cjs");
|
|
const TR = load("spreadsheet_trace");
|
|
const ST = load("spreadsheet_eval_steps");
|
|
const EC = load("spreadsheet_errcheck");
|
|
|
|
const results = {};
|
|
let group = "";
|
|
const eq = (label, got, want) => {
|
|
const g = JSON.stringify(got);
|
|
const w = JSON.stringify(want);
|
|
if (g !== w) results[group].push(`${label}: got ${g} want ${w}`);
|
|
};
|
|
const section = (name, fn) => {
|
|
group = name;
|
|
results[name] = [];
|
|
try {
|
|
fn();
|
|
} catch (e) {
|
|
results[name].push(`예외: ${e && e.stack ? e.stack : e}`);
|
|
}
|
|
};
|
|
|
|
const cellsOf = (cells) => {
|
|
const 칸 = {};
|
|
for (const [k, v] of Object.entries(cells))
|
|
칸[k] = typeof v === "string" && v.startsWith("=") ? { 식: v.slice(1) } : { 값: v };
|
|
return 칸;
|
|
};
|
|
function book(cells, sheet2) {
|
|
const 시트 = [{ id: "s1", 이름: "본", 칸: cellsOf(cells) }];
|
|
if (sheet2) 시트.push({ id: "s2", 이름: "둘", 칸: cellsOf(sheet2) });
|
|
return { 종류: "통합문서", 판: 1, 이름: "t", 서식: [{}], 시트 };
|
|
}
|
|
const at = (a1, 시트 = "s1") => ({ 시트, ...A.parseA1(a1) });
|
|
const name = (a) => `${a.시트}!${A.toA1(a.r, a.c)}`;
|
|
const arrowText = (a) =>
|
|
`${a.from.시트}!${A.rangeToA1(a.from.범위)}>${name(a.to)}${a.오류 ? "!" : ""}`;
|
|
|
|
// ── 1. 선행 · 종속 추적 ──────────────────────────────────────────────────────
|
|
section("trace", () => {
|
|
const b = book(
|
|
{ A1: 1, A2: 2, B1: "=A1+A2", C1: "=B1*2", D1: "=SUM(A1:A2)", A3: "=1/0", B3: "=A3+1" },
|
|
{ E1: "=본!C1" },
|
|
);
|
|
const e = G.createCalcEngine(b);
|
|
const st = { arrows: [], opened: new Set() };
|
|
// 선행: C1 → 한 단계씩
|
|
eq("선행 1", TR.traceStep(b, e, st, "선행", at("C1")), true);
|
|
eq("선행 1 화살", st.arrows.map(arrowText), ["s1!B1>s1!C1"]);
|
|
eq("선행 2", TR.traceStep(b, e, st, "선행", at("C1")), true);
|
|
eq("선행 2 화살", st.arrows.map(arrowText).slice(1), ["s1!A1>s1!B1", "s1!A2>s1!B1"]);
|
|
eq("선행 3 더 없음", TR.traceStep(b, e, st, "선행", at("C1")), false);
|
|
|
|
// 종속: A1 → B1 · D1(범위로) → C1 → 둘!E1(다른 시트)
|
|
const sd = { arrows: [], opened: new Set() };
|
|
TR.traceStep(b, e, sd, "종속", at("A1"));
|
|
eq("종속 1", sd.arrows.map((a) => name(a.to)).sort(), ["s1!B1", "s1!D1"]);
|
|
TR.traceStep(b, e, sd, "종속", at("A1"));
|
|
eq("종속 2", sd.arrows.map((a) => name(a.to)).slice(2), ["s1!C1"]);
|
|
TR.traceStep(b, e, sd, "종속", at("A1"));
|
|
eq("종속 3 다른 시트", sd.arrows.map(arrowText).slice(3), ["s1!C1>s2!E1"]);
|
|
eq("종속 4 더 없음", TR.traceStep(b, e, sd, "종속", at("A1")), false);
|
|
|
|
// 범위 선행 · 오류 화살
|
|
const sr = { arrows: [], opened: new Set() };
|
|
TR.traceStep(b, e, sr, "선행", at("D1"));
|
|
eq("범위 화살", sr.arrows.map(arrowText), ["s1!A1:A2>s1!D1"]);
|
|
const se = { arrows: [], opened: new Set() };
|
|
TR.traceStep(b, e, se, "선행", at("B3"));
|
|
eq("오류 칸 화살 빨강", se.arrows.map(arrowText), ["s1!A3>s1!B3!"]);
|
|
eq("값 칸은 선행 없음", TR.traceStep(b, e, { arrows: [], opened: new Set() }, "선행", at("A1")), false);
|
|
eq(
|
|
"dependentsOf",
|
|
TR.dependentsOf(b, e, "s1", 0, 1).map(name),
|
|
["s1!C1"],
|
|
);
|
|
});
|
|
|
|
// ── 2. 수식 계산 걸음 ────────────────────────────────────────────────────────
|
|
const stepsOf = (cells, a1) => {
|
|
const b = book(cells);
|
|
const e = G.createCalcEngine(b);
|
|
const p = A.parseA1(a1);
|
|
return ST.evalSteps(b, e, "s1", p.r, p.c).map((s) =>
|
|
s.밑줄 ? `${s.글.slice(0, s.밑줄[0])}[${s.글.slice(s.밑줄[0], s.밑줄[1])}]${s.글.slice(s.밑줄[1])}` : s.글,
|
|
);
|
|
};
|
|
section("eval_steps", () => {
|
|
eq("차례 · 빈 칸 0 · 범위는 함수가", stepsOf({ A1: 3, A3: 0, B1: "=A1*2+SUM(A1:A3)-A2" }, "B1"), [
|
|
"[A1]*2+SUM(A1:A3)-A2",
|
|
"[3*2]+SUM(A1:A3)-A2",
|
|
"6+[SUM(A1:A3)]-A2",
|
|
"[6+3]-A2",
|
|
"9-[A2]",
|
|
"[9-0]",
|
|
"9",
|
|
]);
|
|
eq("괄호 안 밑줄", stepsOf({ A1: 3, B1: "=(A1+1)*2" }, "B1"), [
|
|
"([A1]+1)*2",
|
|
"([3+1])*2",
|
|
"[4*2]",
|
|
"8",
|
|
]);
|
|
eq("IF 는 첫 인자만 먼저 · 안 고른 쪽 오류 무시", stepsOf({ A1: 3, B1: '=IF(A1>2,"큼",1/0)' }, "B1"), [
|
|
'IF([A1]>2,"큼",1/0)',
|
|
'IF([3>2],"큼",1/0)',
|
|
'[IF(TRUE,"큼",1/0)]',
|
|
'"큼"',
|
|
]);
|
|
eq("글 따옴표", stepsOf({ A1: 'a"b', B1: '=A1&"x"' }, "B1"), [
|
|
'[A1]&"x"',
|
|
'["a""b"&"x"]',
|
|
'"a""bx"',
|
|
]);
|
|
eq("음수 글자는 그대로", stepsOf({ B1: "=-5+1" }, "B1"), ["[-5+1]", "-4"]);
|
|
eq("오류 전파", stepsOf({ A1: 0, B1: "=1/A1+1" }, "B1"), [
|
|
"1/[A1]+1",
|
|
"[1/0]+1",
|
|
"[#DIV/0!+1]",
|
|
"#DIV/0!",
|
|
]);
|
|
eq("값 칸은 null", ST.evalSteps(book({ A1: 1 }), G.createCalcEngine(book({ A1: 1 })), "s1", 0, 0), null);
|
|
eq("상수 식", stepsOf({ B1: "=5" }, "B1"), ["5"]);
|
|
});
|
|
|
|
// ── 3. 오류 검사 까닭 ────────────────────────────────────────────────────────
|
|
section("errcheck", () => {
|
|
const b = book({
|
|
A1: "1,234",
|
|
A2: "abc",
|
|
A3: "12a",
|
|
A4: 5,
|
|
A5: "=1/0",
|
|
A6: "=A7",
|
|
A7: "=A6",
|
|
A8: " 1e3 ",
|
|
A9: "=A5+1",
|
|
A10: "=A4*2",
|
|
});
|
|
const e = G.createCalcEngine(b);
|
|
const w = (a1) => {
|
|
const p = A.parseA1(a1);
|
|
const x = EC.cellWarning(b, e, "s1", p.r, p.c);
|
|
return x && x.종류;
|
|
};
|
|
eq(
|
|
"종류",
|
|
["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B1"].map(w),
|
|
["글숫자", null, null, null, "오류", "순환", "순환", "글숫자", "오류", null, null],
|
|
);
|
|
const why = EC.cellWarning(b, e, "s1", 4, 0).까닭;
|
|
eq("#DIV/0! 까닭", why.startsWith("#DIV/0! 0 으로 나눔"), true);
|
|
eq("순환 까닭", EC.cellWarning(b, e, "s1", 5, 0).까닭.includes("A6 → A7"), true);
|
|
eq("numberLike", ["1,234", " 1e3 ", "1,23", ".5", "-", "+7"].map(EC.numberLike), [
|
|
1234,
|
|
1000,
|
|
null,
|
|
0.5,
|
|
null,
|
|
7,
|
|
]);
|
|
});
|
|
|
|
// ── 4. 이름 정의 풀이 ────────────────────────────────────────────────────────
|
|
section("names", () => {
|
|
const { C, E, F } = require("./helper_spreadsheet_engine.cjs");
|
|
const N = load("spreadsheet_names");
|
|
const show = (v) => (v === null ? null : E.isError(v) ? v.error : E.isFrac(v) ? F.fracToString(v) : v);
|
|
const b = book(
|
|
{ A1: 1, A2: 2, A3: 3, B1: 10, C1: "=SUM(값들)", C2: "=단가*2", C3: "=없는이름+1", C4: "=SUM(값들)*단가" },
|
|
{ D1: "=SUM(값들)", D2: "=SUM(둘값)", E1: 100 },
|
|
);
|
|
b.시트[0].이름정의 = [
|
|
{ 이름: "값들", 범위: A.parseRange("A1:A3") },
|
|
{ 이름: "단가", 범위: A.parseRange("B1") },
|
|
];
|
|
b.시트[1].이름정의 = [{ 이름: "둘값", 범위: A.parseRange("E1") }];
|
|
const e = G.createCalcEngine(b);
|
|
const v = (a1, s = "s1") => {
|
|
const p = A.parseA1(a1);
|
|
return show(e.value(s, p.r, p.c));
|
|
};
|
|
eq("풀이", ["C1", "C2", "C3", "C4"].map((a) => v(a)), ["6", "20", "#NAME?", "60"]);
|
|
eq("다른 시트에서 · 자기 시트 이름", [v("D1", "s2"), v("D2", "s2")], ["6", "100"]);
|
|
// 이름 범위 칸을 고치면 따라 풂
|
|
C.applyCommand(b, { 종류: "칸", 시트: "s1", 칸: { A1: { 값: 5 } } });
|
|
e.update([{ 시트: "s1", r: 0, c: 0 }]);
|
|
eq("딸린 칸 다시 풂", [v("C1"), v("C4"), v("D1", "s2")], ["10", "100", "10"]);
|
|
// 식 계산 걸음도 이름을 품
|
|
const p = A.parseA1("C4");
|
|
eq(
|
|
"계산 걸음",
|
|
ST.evalSteps(b, e, "s1", p.r, p.c).map((s) => s.글),
|
|
["SUM(값들)*단가", "10*단가", "10*10", "100"],
|
|
);
|
|
|
|
// 이름 바꾸기 · 지우기(sub4 명령 → 시트통째 → rebuild)
|
|
const ctx = {
|
|
get book() {
|
|
return b;
|
|
},
|
|
sheet: () => b.시트[0],
|
|
dispatch: (cmd) => {
|
|
C.applyCommand(b, cmd);
|
|
e.rebuild(b);
|
|
},
|
|
};
|
|
eq("이름 바꿈", N.renameName(ctx, "값들", "수량"), null);
|
|
eq("바꾼 뒤", [b.시트[0].칸.C1.식, v("C1"), v("D1", "s2")], ["SUM(수량)", "10", "10"]);
|
|
N.deleteName(ctx, "단가");
|
|
eq("지운 뒤 #NAME?", [v("C2"), v("C4")], ["#NAME?", "#NAME?"]);
|
|
|
|
// 행 넣기 · 지우기 때 이름 범위가 따라감 · 되돌리기
|
|
const fx = C.applyCommand(b, { 종류: "행넣기", 시트: "s1", at: 0, 수: 1 });
|
|
e.rebuild(b);
|
|
eq("넣은 뒤 범위", A.rangeToA1(b.시트[0].이름정의[0].범위), "A2:A4");
|
|
eq("넣은 뒤 값", [v("C2"), v("D1", "s2")], ["10", "10"]);
|
|
C.applyCommand(b, fx.undo);
|
|
e.rebuild(b);
|
|
eq("되돌린 범위", A.rangeToA1(b.시트[0].이름정의[0].범위), "A1:A3");
|
|
C.applyCommand(b, { 종류: "행지우기", 시트: "s1", at: 1, 수: 1 });
|
|
e.rebuild(b);
|
|
eq("가운데 지움", [A.rangeToA1(b.시트[0].이름정의[0].범위), v("C1")], ["A1:A2", "8"]);
|
|
C.applyCommand(b, { 종류: "열지우기", 시트: "s1", at: 0, 수: 1 });
|
|
e.rebuild(b);
|
|
eq("통째 지움 → 이름 빠짐", [b.시트[0].이름정의.length, v("B1")], [0, "#NAME?"]);
|
|
});
|
|
|
|
process.stdout.write(JSON.stringify(results));
|