/* 804-2 줄 쌓기 헬퍼 — common_util_line_stack.ts · 계산기 · 캐시를 그 자리에서 트랜스파일해 Node 로 돌림. * stdin = {할일, 문서들 [{문서, 연결?}]} · stdout = 할일마다 답 목록 */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const cache = new Map(); function load(at) { if (cache.has(at)) return cache.get(at); const js = ts.transpileModule(fs.readFileSync(at, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(at, box.exports); const need = (p) => p.startsWith(".") ? load(path.resolve(path.dirname(at), `${p}.ts`)) : require(p); new Function("exports", "module", "require", js)(box.exports, box, need); return box.exports; } const util = (name) => load(path.join(__dirname, "..", "..", "common_util", `${name}.ts`)); const ls = util("common_util_line_stack"); const calc = util("common_util_formula_doc_calc"); const dc = util("common_util_formula_doc_cache"); const money = (a) => ({ 노무비: a.노무비, 재료비: a.재료비, 경비: a.경비, 계: a.계, 결과: a.결과 }); /** 왕복 — 문서 → 줄 → 문서 · 덮음(모든 금액 줄 한 번 · 차림 기호 한 번) · 차림 고침 뒤 다시 읽어 같음 */ function roundTrip({ 문서 }) { const items = ls.stackOf(문서); const back = ls.docOf(문서, items); const ms = items.flatMap((it) => it.줄.filter((l) => l.꼴 === "금액").map((l) => l.i)); const syms = items.flatMap((it) => it.줄.filter((l) => l.꼴 === "공식").flatMap((l) => l.기호)); const hidden = ls.hiddenOf(문서); const want = ls.symbols(문서).filter((x) => !hidden.has(x)); // 차림 고침 — 항목마다 글 줄 · 첫 항목 가나다 · 맨 앞 새 항목 제목 const edited = JSON.parse(JSON.stringify(items)); edited.forEach((it, k) => it.줄.push({ 꼴: "글", 글: `< 근거 ${k} >` })); if (edited[0]) edited[0].가나다 = true; edited.unshift({ 제목: "시험 머리", 줄: [{ 꼴: "글", 글: "※ 메모" }] }); const laid = ls.docOf(문서, edited); const again = ls.stackOf(laid); return { 같음: JSON.stringify(back) === JSON.stringify(문서), 쌓기: "쌓기" in back, 금액: ms.length === (문서.금액 || []).length && new Set(ms).size === ms.length, 기호: syms.length === want.length && new Set(syms).size === syms.length && want.every((x) => syms.includes(x)), 차림: JSON.stringify(again) === JSON.stringify(edited), 차림문서: laid, 못: ls.notesOf(문서), }; } /** 셈 — 연결 기록만으로(화면과 같은 길) · 기계 · 단가산출 펼침 셈 */ function money_({ 문서, 연결 }) { const c = dc.createDocCache(); c.merge(연결); const a = calc.calcDoc(문서, c.links, {}); const subs = []; (문서.금액 || []).forEach((l, i) => { const kind = ls.kindOf(문서, i); if (kind !== "기계" && kind !== "단가산출") return; const s = ls.priceSym(l); const link = (문서.변수 || []).find((v) => ls.symbols({ 변수: [v] })[0] === s)?.연결; const sub = c.links.doc(link.키); const ins = ls.subInputs(link, a.값 || {}); const got = calc.calcDoc(sub, c.links, ins); subs.push({ i, 갈래: kind, 키: link.키, 입력: ins, ...money(got), 값: (a.값 || {})[s] }); }); return { ...money(a), 펼침: subs, 누락: c.takeMissing() }; } /** 틀 펼침 — 단가산출 결과 연결 기호를 이 문서 식으로 옮김 → 셈 같음 · 기호 없으면 그런 기호 전부(하나씩) * 고침 = 펼친 뒤 [{기호, 글}](setRow) · 지움 = 펼친 뒤 지울 기호(dropSyms) */ function unfold({ 문서, 연결, 기호, 고침, 지움 }) { const c = dc.createDocCache(); c.merge(연결); const before = calc.calcDoc(문서, c.links, {}); const idOf = (v) => ls.symbols({ 변수: [v] })[0]; const targets = 기호 ? [기호] : (문서.변수 || []) .filter((v) => v.연결?.갈래 === "단가산출" && v.연결.키 && !v.연결.키.includes("{")) .filter((v) => c.links.doc(v.연결.키)?.결과) .map(idOf); return targets.map((id) => { const link = 문서.변수.find((v) => idOf(v) === id).연결; const doc = JSON.parse(JSON.stringify(문서)); const moved = ls.unfold(doc, c.links.doc(link.키), id); const after = calc.calcDoc(doc, c.links, {}); for (const g of 고침 || []) ls.setRow(doc, g.기호, g.글); if (지움) ls.dropSyms(doc, 지움); const edited = calc.calcDoc(doc, c.links, {}); const items = ls.stackOf(doc); return { 기호: id, 전: money(before), 후: money(after), 고친: money(edited), 표지: (edited.표지 || []).map((m) => m.갈래), 옮김: moved, 틀꼴: ls.shapeOf(doc, moved), 왕복: JSON.stringify(ls.docOf(doc, items)) === JSON.stringify(doc), 문서: doc, }; }); } /* 804-3 고르기 — common_util_line_stack_pick.ts */ const pk = util("common_util_line_stack_pick"); const idOf = (v) => ls.symbols({ 변수: [v] })[0]; const varOf = (doc, id) => (doc.변수 || []).find((v) => idOf(v) === id); /** 표 고름 — 앞 = [{기호, 조건}](먼저 조건을 바꿔 둠) · 기호의 표에서 줄 · 칸 고름 → 연결 · 값 · 금액 */ function tablePick({ 문서, 연결, 기호, 줄, 칸, 앞 }) { const doc = JSON.parse(JSON.stringify(문서)); for (const f of 앞 || []) varOf(doc, f.기호).연결.조건 = f.조건; const c = dc.createDocCache(); c.merge(연결); const link = varOf(doc, 기호).연결; const t = 연결.표[link.표]; const before = calc.calcDoc(doc, c.links, {}); const now = pk.currentRow(t, link, before.값 || {}); const bound = pk.boundConds(doc, link); pk.pickCell(doc, 기호, t, t.줄[줄], 칸); const after = calc.calcDoc(doc, c.links, {}); return { 전줄: now, 묶임: bound, 연결: varOf(doc, 기호).연결, 값: (after.값 || {})[기호], 후줄: pk.currentRow(t, varOf(doc, 기호).연결, after.값 || {}), ...money(after), 문서: doc, }; } /** 찾아 넣기 — 새 항목에 고른 것 차례로 · 바꿈 = [{차례, 고른}](넣은 줄 다시 고름) */ function putPicked({ 문서, 넣을, 바꿈 }) { const doc = JSON.parse(JSON.stringify(문서)); const items = ls.stackOf(doc); items.push({ 제목: "시험 넣기", 가나다: true, 줄: [] }); const base = (doc.금액 || []).length; const ids = 넣을.map((x) => pk.addPicked(doc, items[items.length - 1], x.갈래, x.고른)); for (const r of 바꿈 || []) pk.repick(doc, base + r.차례, ids[r.차례], 넣을[r.차례].갈래, r.고른); const laid = ls.docOf(doc, items); return { 문서: laid, 기호: ids, 처음: base, 갈래: ids.map((_, k) => ls.kindOf(laid, base + k)), 수준: pk.stackLevels(ls.stackOf(laid)), }; } /** 있는 줄 다시 고름 — {문서, 줄, 기호, 갈래, 고른} → 문서 */ function repickOne({ 문서, 줄, 기호, 갈래, 고른 }) { const doc = JSON.parse(JSON.stringify(문서)); pk.repick(doc, 줄, 기호, 갈래, 고른); return doc; } /* 804-4 복사해 펼침 — common_util_line_stack_copy.ts · 받음 = 서버 조각(차례로 넣음) */ const cp = util("common_util_line_stack_copy"); function takeIn({ 문서, 받음 }) { const doc = JSON.parse(JSON.stringify(문서)); const items = ls.stackOf(doc); const was = items.length; for (const t of 받음) cp.addTaken(doc, items, t); const laid = ls.docOf(doc, items); const again = ls.stackOf(laid); return { 문서: laid, 항목: again.slice(was).map((it) => ({ 제목: it.제목, 가나다: !!it.가나다, 줄: it.줄.map((l) => l.꼴 === "금액" ? `금액:${laid.금액[l.i].이름}` : l.꼴 === "글" ? `글:${l.글}` : `공식:${l.기호.length}`, ), })), 옵션: (laid.변수 || []).filter((v) => ls.isOption(v)).map(idOf), 못: ls.notesOf(laid), 왕복: JSON.stringify(ls.docOf(laid, again)) === JSON.stringify(laid), }; } /* 804-5 끌어 옮김 · 번지 읽는 이름 — 옮김 = [[from, to] …](줄 "i.j" → "k.p" · 항목 "i" → "p") */ function moveAll({ 문서, 옮김 }) { let doc = JSON.parse(JSON.stringify(문서)); for (const [a, b] of 옮김) { const items = ls.stackOf(doc); if (a.includes(".")) ls.moveLine(items, ...a.split(".").map(Number), ...b.split(".").map(Number)); else ls.moveItem(items, Number(a), Number(b)); doc = ls.docOf(doc, items); } const items = ls.stackOf(doc); return { 문서: doc, 항목: items.map((it) => ({ 제목: it.제목, 줄: it.줄.map((l) => l.꼴 === "금액" ? doc.금액[l.i].이름 : l.꼴 === "글" ? l.글 : l.기호.join(","), ), })), }; } const placeOf = ({ 문서 }) => cp.placeNames(문서, ls.stackOf(문서)); /* 683 표 값 기호 화면 이름 — 별칭 · 글 왕복(화면 이름 → 기호 = 처음 식과 같은 나무) */ const pk2 = util("common_util_line_stack_pick"); const fd = util("common_util_formula_doc"); function aliasOf({ 문서 }) { const alias = pk2.tableAlias(문서); const texts = [ ...(문서.식 || []).map((l) => l.식), ...(문서.금액 || []).flatMap((l) => [l.수량, l.단가]), ].filter((t) => typeof t === "string" && t); const bad = []; for (const t of texts) { let want; try { want = fd.textOf(fd.parse(t)); } catch { continue; } const shown = pk2.aliasText(t, alias); const back = pk2.aliasText(shown, alias, true); if (fd.textOf(fd.parse(back)) !== want) bad.push(t); if (Object.keys(alias).some((id) => fd.names(fd.parse(shown)).includes(id))) bad.push(`남음:${t}`); } return { 별칭: alias, 틀림: bad }; } /* 692-1 표 값 직접 침 — 숫자 두 번 · 식 한 번 쳐도 별칭 같음 · 고친 문서(서버 셈 대조) */ function directOf({ 문서 }) { const before = pk2.tableAlias(문서); const doc = JSON.parse(JSON.stringify(문서)); const ids = Object.keys(before); for (const id of ids) ls.setRow(doc, id, "7"); for (const id of ids) ls.setRow(doc, id, "8"); const typed = pk2.tableAlias(doc); const expr = JSON.parse(JSON.stringify(doc)); for (const id of ids) ls.setRow(expr, id, "8 + 0"); const kinds = ids.map((id) => varOf(doc, id).연결.갈래); return { 전: before, 숫자: typed, 식: pk2.tableAlias(expr), 갈래: kinds, 문서: doc }; } /* 695 결정된 식 — 만약 든 식 줄마다 결정된 식 값 = 원 식 값 = 셈 답 · 남은 만약 · 근거 수 */ const vw = util("common_util_formula_doc_view"); function decideOf({ 문서, 연결 }) { const c = dc.createDocCache(); c.merge(연결); const vals = calc.calcDoc(문서, c.links, {}).값 || {}; const out = []; for (const l of 문서.식 || []) { const t = vw.tryParse(l.식); if (!t || !vw.hasIf(t)) continue; const d = vw.decide(t, vals); const a = vw.evalNode(t, vals); const b = vw.evalNode(d, vals); out.push({ 기호: l.기호, 같음: a === undefined ? b === undefined : calc.same(a, b), 답: a === undefined || calc.same(a, vals[l.기호]), 남은만약: vw.hasIf(d), 근거: vw.whyOf(t, vals).length, 글: fd.textOf(d), }); } return out; } const input = JSON.parse(fs.readFileSync(0, "utf8")); const job = { 왕복: roundTrip, 셈: money_, 펼침: unfold, 표고름: tablePick, 넣기: putPicked, 바꿈: repickOne, 복사: takeIn, 옮김: moveAll, 번지: placeOf, 별칭: aliasOf, 직접: directOf, 결정: decideOf, }[input.할일]; process.stdout.write(JSON.stringify(input.문서들.map(job)));