Files
Aislo/resources/tester/spreadsheet/helper_spreadsheet_analysis.cjs
T
eomsangdonandClaude Opus 5.5 6acc8d0c52 feat(spreadsheet): 수식 분석 — 선행 · 종속 추적 화살표 · 수식 계산 단계 보기 · 오류 검사 ⚠ 풍선
- 추적 화살표(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
2026-09-27 21:14:40 +09:00

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));