- 추적 화살표(spreadsheet_trace.ts): 누를 때마다 한 단계 · 범위는 상자 · 오류 칸 화살 빨강 · 다른 시트 점선 · 지우기 - 수식 계산 창(spreadsheet_eval_steps.ts): 왼쪽부터 한 마디씩 값으로 · 밑줄 · IF 류 첫 인자만 먼저 · 값은 엔진 풀이 - 오류 검사(spreadsheet_errcheck.ts): 순환 · 식 오류 · 숫자처럼 보이는 글 까닭 · ⚠ 메뉴(숫자로 변환 · 계산 단계 · 선행 추적 · 오류 무시) - 격자 초록 세모는 C(sub6) · 잇기는 sub7 — 머리 주석에 잇는 법 - 엔진: evalSafe 내보냄 · formulaToText 에 갈음 · 밑줄 자리 · 계약 파일 목록 줄만 더함 - 시험: test_spreadsheet_analysis.py(3 묶음) · harness_analysis(ORCA) · 시험 틀은 inspect 로 문맥 꺼냄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF
168 lines
6.3 KiB
JavaScript
168 lines
6.3 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,
|
|
]);
|
|
});
|
|
|
|
process.stdout.write(JSON.stringify(results));
|