/* ============================================================================= * common_util_formula_doc_view.ts * 수식 문서 그림용 값(PLAN 802-3 · 802-4b-2 · 요구 663-1) — 화면 · DOM · fetch 없음(거울 시험이 Node 로 돌림) * tryParse · evalNode(탄 갈래 값) · takenOf · leafNum · offZero / offLine(선택으로 꺼진 줄) * 셈 정본은 계산기(`common_util_formula_doc_calc.ts`) — 여기는 그림 · 숨김 판정만 * ========================================================================== */ import { DocError, parse, type Node } from "./common_util_formula_doc"; import { binary, cut, isNum } from "./common_util_formula_doc_calc"; export type Values = Record; /** 식 글 → 나무(못 풀면 null) */ export function tryParse(text: unknown): Node | null { if (text === null || text === undefined || String(text).trim() === "") return null; try { return parse(text); } catch (e) { if (e instanceof DocError) return null; throw e; } } /** 그림용 값 — 모르면 undefined */ export function evalNode(n: Node, vals: Values): unknown { const ev = (x: Node): unknown => evalNode(x, vals); try { switch (n[0]) { case "num": case "str": return n[1]; case "var": { const v = vals[n[1]]; return v === null ? undefined : v; } case "field": { const b = ev(n[1]); return b && typeof b === "object" ? (b as Values)[n[2]] : undefined; } case "neg": { const a = ev(n[1]); return isNum(a) ? -a : undefined; } case "pct": { const a = ev(n[1]); return isNum(a) ? a / 100 : undefined; } case "bin": { const a = ev(n[2]); if (a === undefined) return undefined; if (n[1] === "그리고" && !a) return a; if (n[1] === "또는" && a) return a; const b = ev(n[3]); if (b === undefined) return undefined; if (n[1] === "그리고" || n[1] === "또는") return b; const got = binary(n[1], a, b); return typeof got === "number" && !Number.isFinite(got) ? undefined : got; } case "call": { const [, name, items] = n; if (name === "만약") { const t = items.length === 3 ? ev(items[0]) : undefined; return t === undefined ? undefined : ev(t ? items[1] : items[2]); } const xs = items.map(ev); if (!xs.every(isNum)) return undefined; if (name === "최소") return Math.min(...(xs as number[])); if (name === "최대") return Math.max(...(xs as number[])); return cut(xs[0] as number, { 방법: name, 자리: Math.trunc((xs[1] as number) ?? 0) }); } } } catch { return undefined; } } /** 만약 사슬이 탄 끝 갈래가 수 하나면 그 수(만약이 아니거나 모르면 undefined) */ export function leafNum(n: Node, vals: Values): number | undefined { let at = n; let hop = 0; for (let pick = takenOf(at, vals); pick; pick = takenOf(at, vals), hop++) at = (at as ["call", string, Node[]])[2][pick]; return hop && at[0] === "num" ? at[1] : undefined; } /** 선택으로 꺼진 0 — 만약이 탄 끝 갈래가 0 · 곱 인수(× · ÷ 위) 하나가 꺼진 0 · 식 줄 기호면 그 줄 식으로 거침 * (663-1 · 802-4b-2) — 입력 · 숫자 0(구멍땜 0 개소) · 글로 적은 0 은 꺼진 것 아님 */ export function offZero( n: Node | null, vals: Values, lines: ReadonlyMap = new Map(), path: ReadonlySet = new Set(), ): boolean { if (!n) return false; let at = n; let hop = 0; for (let pick = takenOf(at, vals); pick; pick = takenOf(at, vals), hop++) at = (at as ["call", string, Node[]])[2][pick]; if (hop && at[0] === "num") return at[1] === 0; const off = (x: Node): boolean => offZero(x, vals, lines, path); switch (at[0]) { case "var": return ( lines.has(at[1]) && !path.has(at[1]) && offZero(tryParse(lines.get(at[1])), vals, lines, new Set([...path, at[1]])) ); case "neg": case "pct": return off(at[1]); case "bin": if (at[1] === "*") return off(at[2]) || off(at[3]); if (at[1] === "/") return off(at[2]); if (at[1] === "+" || at[1] === "-") return off(at[2]) && off(at[3]); return false; default: return false; } } /** 식 줄 표 {기호: 식} — `offLine` 이 계산 변수를 거칠 때 */ export const lineMap = (doc: { 식?: { 기호?: unknown; 식?: unknown }[] }): Map => new Map((doc.식 ?? []).map((l) => [String(l.기호 ?? ""), l.식])); /** 선택으로 꺼진 금액 줄 — 수량 · 단가가 선택으로 꺼진 0(`offZero` · 고침 모드에서만 보임) */ export function offLine( line: { 수량?: unknown; 단가?: unknown }, vals: Values, lines: ReadonlyMap = new Map(), ): boolean { return [line.수량, line.단가].some((t) => offZero(tryParse(t), vals, lines)); } /** 만약 — 탄 갈래 차례(1 참 · 2 거짓 · 0 모름) */ export function takenOf(n: Node, vals: Values): 0 | 1 | 2 { if (n[0] !== "call" || n[1] !== "만약" || n[2].length !== 3) return 0; const t = evalNode(n[2][0], vals); return t === undefined ? 0 : t ? 1 : 2; } /** 금액 줄 i 가 부르는 비목 합(노무비 · 재료비 · 경비 = 앞 줄 누계 · 합밖 · 제외 단계 줄 뺌) * — 계산기 `money` 의 running 과 같은 값(셈 답 `줄` 의 비목에서 다시 모음 · 802-4b-3) */ export function moneyScope(rows: unknown, i: number): Values { const acc: Record = { 노무비: 0, 재료비: 0, 경비: 0 }; ((rows ?? []) as { 비목?: Record; 합밖?: unknown }[]) .slice(0, i) .forEach((r) => { if (r.합밖) return; for (const k of Object.keys(acc)) if (isNum(r.비목?.[k])) acc[k] += r.비목![k] as number; }); return acc; }