/* 수식 분석 엔진 시험 헬퍼 — 추적 화살표 단계 · 수식 계산 걸음 · 오류 검사 까닭을 묶음마다 돌려 틀린 것 목록을 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));