/* ============================================================================= * M02_CF_Eval.ts * 단가산출 다이어그램 계산 · 검사 — 함수 상자는 차례대로 시트를 우리 스프레드시트 엔진으로 셈 * (받는 칸에 앞 값을 넣음) · 출력 상자를 비용 종류별로 모아 원가 · DOM 없음 * 엔진을 정적으로 가져옴 — 화면(Entry)은 CostFlow 를 import() 로 늦게 불러 첫 묶음에 안 들어가게 * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { parseA1, toA1 } from "../../A00_Common/spreadsheet/spreadsheet_address"; import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc"; import type { CalcValue, Cell, Sheet } from "../../A00_Common/spreadsheet/spreadsheet_types"; import { OUTPUT_PORT, portsIn, portsOut, type CfBox, type CfLogic } from "./M02_CF_Model"; /** 알약 값 — 수 · 글 · 참거짓 · 빈 값 · 표(머리 줄 포함 칸 덩어리) */ export type CfValue = number | string | boolean | null | (number | string | null)[][]; /** 입력 값 묶음 — 상자 id → 알약 이름 → 값(입력 상자 요소 값 · 표 · 변수 값 · 함수 상자 변수 값) */ export type CfValues = Record>; type Scalar = number | string | boolean | null; export interface CfEvalResult { /** 상자 id → 내보내는 알약 이름 → 값 */ ports: Record>; /** 출력 상자 id → 값 */ outputs: Record; cost: { 노무비: number; 재료비: number; 경비: number; /** 비용 아닌 출력(값) — 출력 상자 이름 → 값 */ 값들: Record; 계: number; }; /** 쉬운 말 */ errors: string[]; } const TEXT = { /** {box} {cell} {err} */ CellError: [ "「{box}」 {cell} 칸 계산이 막힘 — {err}", "「{box}」 cell {cell} can't be worked out — {err}", ], Cycle: ["돌고 도는 참조", "circular reference"], /** {box} */ Loop: ["「{box}」 이 돌고 도는 선에 걸림", "「{box}」 is in a loop of lines"], NotNumber: ["「{box}」 에 숫자가 아닌 값이 들어옴", "「{box}」 got a value that isn't a number"], /** {box} {pill} */ EmptyIn: [ "「{box}」 의 받는 알약 「{pill}」 에 선이 없음", "Pill 「{pill}」 of 「{box}」 has no line", ], NoCell: ["「{box}」 의 출력 칸이 정해지지 않음", "Output cell of 「{box}」 is not set"], NoOutput: ["출력 상자가 없음", "There is no output box"], NoInCell: [ "「{box}」 의 받는 알약 「{pill}」 을 칸에 놓지 않음", "Pill 「{pill}」 of 「{box}」 is not placed in a cell", ], NoValue: ["「{box}」 의 「{pill}」 값이 아직 없음", "「{pill}」 of 「{box}」 has no value yet"], } as const; const tl = (key: keyof typeof TEXT, fill: Record = {}): string => Object.entries(fill).reduce( (s, [k, v]) => s.replace(`{${k}}`, v), TEXT[key][currentLanguageIndex === 1 ? 1 : 0] as string, ); /* ── 차례 ───────────────────────────────────────────────────────────────── */ /** 함수 상자 차례 — 앞 함수가 먼저 · 돌고 도는 상자는 loop 로 */ function order(lg: CfLogic): { funcs: CfBox[]; loop: CfBox[] } { const funcs = lg.boxes.filter((b) => b.kind === "function"); const done: CfBox[] = []; const left = new Set(funcs.map((f) => f.id)); for (let moved = true; moved && left.size;) { moved = false; for (const f of funcs) { if (!left.has(f.id)) continue; const waits = lg.links.some((l) => l.to.box === f.id && left.has(l.from.box)); if (waits) continue; done.push(f); left.delete(f.id); moved = true; } } return { funcs: done, loop: funcs.filter((f) => left.has(f.id)) }; } /* ── 칸 ─────────────────────────────────────────────────────────────────── */ function putValue(칸: Record, cell: string, value: Scalar): void { const style = 칸[cell]?.서식; if (value === null) { if (style) 칸[cell] = { 서식: style }; else delete 칸[cell]; } else 칸[cell] = { 값: value, ...(style ? { 서식: style } : {}) }; } /** 칸에 값 넣기 — 표는 cell 을 왼위로 펼침 */ function put(칸: Record, cell: string, value: CfValue): void { const at = parseA1(cell); if (!at) return; if (!Array.isArray(value)) return putValue(칸, toA1(at.r, at.c), value); value.forEach((line, dr) => line.forEach((x, dc) => putValue(칸, toA1(at.r + dr, at.c + dc), x))); } const fromCalc = (v: CalcValue | undefined): Scalar => { if (!v) return null; if ("수" in v) return Number(v.수); if ("글" in v) return v.글; if ("참" in v) return v.참; return v.오류; }; /* ── 계산 ───────────────────────────────────────────────────────────────── */ /** 입력 상자 알약 값 — 넘긴 값 · 없으면 변수 예시값 · 사용자 요소 값 */ function inputPorts(lg: CfLogic, box: CfBox, values: CfValues): Record { const given = values[box.id] ?? {}; const pills = portsOut(box); const out: Record = {}; const inp = box.input; if (!inp) return out; // portsOut 차례 = 요소 · 표 · 변수 const sources: CfValue[] = [ ...(inp.mode === "group" ? inp.elements.map((e) => lg.userElements.find((u) => u.id === e.ref)?.value ?? null) : []), ...(inp.mode === "group" && inp.table ? [null] : []), ...inp.variables.map((v) => v.example), ]; pills.forEach((p, i) => (out[p.name] = p.name in given ? given[p.name] : (sources[i] ?? null))); return out; } /** 함수 상자 하나 — 받는 칸 · 변수 칸에 값을 넣고 시트 하나를 셈 */ function runFunc( box: CfBox, incoming: Record, given: Record, errors: string[], ): Scalar { const func = box.func!; const sheet: Sheet = structuredClone(func.sheet); for (const i of func.inputs) put(sheet.칸, i.cell, incoming[i.port] ?? given[i.port] ?? null); for (const v of func.variables) if (v.cell) put(sheet.칸, v.cell, v.name in given ? given[v.name] : v.example); const values = recalcWorkbook({ 종류: "통합문서", 판: 1, 열: "CF", 서식: [{}], 시트: [sheet] }); for (const [cell, v] of Object.entries(values[sheet.id] ?? {})) if ("오류" in v) { const err = v.오류 === "#CYCLE!" ? `${v.오류} ${tl("Cycle")}` : v.오류; errors.push(tl("CellError", { box: box.name, cell, err })); } const at = func.output.cell.toUpperCase(); const c = sheet.칸[at]; if (!c) return null; return c.식 !== undefined ? fromCalc(values[sheet.id]?.[at]) : (c.값 ?? null); } /** 화면 계산 */ export function evalLogic(lg: CfLogic, values: CfValues = {}): CfEvalResult { const errors: string[] = []; const ports: CfEvalResult["ports"] = {}; for (const b of lg.boxes) if (b.kind === "input") ports[b.id] = inputPorts(lg, b, values); const into = (box: string): Record => Object.fromEntries( lg.links .filter((l) => l.to.box === box) .map((l) => [l.to.name, ports[l.from.box]?.[l.from.name] ?? null]), ); const { funcs, loop } = order(lg); for (const f of loop) errors.push(tl("Loop", { box: f.name })); for (const f of funcs) ports[f.id] = { [f.func!.output.name]: runFunc(f, into(f.id), values[f.id] ?? {}, errors) }; const outputs: CfEvalResult["outputs"] = {}; const cost: CfEvalResult["cost"] = { 노무비: 0, 재료비: 0, 경비: 0, 값들: {}, 계: 0 }; for (const b of lg.boxes.filter((x) => x.kind === "output")) { const v = into(b.id)[OUTPUT_PORT] ?? null; const scalar: Scalar = Array.isArray(v) ? null : v; outputs[b.id] = scalar; const kind = b.output?.cost ?? "값"; if (kind === "값") cost.값들[b.name] = scalar; else if (typeof scalar === "number" && Number.isFinite(scalar)) cost[kind] += scalar; else if (scalar !== null) errors.push(tl("NotNumber", { box: b.name })); } cost.계 = cost.노무비 + cost.재료비 + cost.경비; return { ports, outputs, cost, errors }; } /** 검사 — 빈 받는 자리 · 출력 칸 없음 · 출력 상자 없음 · 값 없는 입력 · 순환 · 오류(쉬운 말 목록) */ export function checkLogic(lg: CfLogic, values: CfValues = {}): string[] { const out: string[] = []; for (const b of lg.boxes) { for (const p of portsIn(b)) if (!lg.links.some((l) => l.to.box === b.id && l.to.name === p.name)) out.push(tl("EmptyIn", { box: b.name, pill: p.name })); if (b.kind === "function" && !parseA1(b.func?.output.cell ?? "")) out.push(tl("NoCell", { box: b.name })); for (const i of b.func?.inputs ?? []) if (!parseA1(i.cell)) out.push(tl("NoInCell", { box: b.name, pill: i.port })); } if (!lg.boxes.some((b) => b.kind === "output")) out.push(tl("NoOutput")); const ev = evalLogic(lg, values); for (const b of lg.boxes.filter((x) => x.kind === "input")) for (const [pill, v] of Object.entries(ev.ports[b.id] ?? {})) if (v === null && lg.links.some((l) => l.from.box === b.id && l.from.name === pill)) out.push(tl("NoValue", { box: b.name, pill })); return [...out, ...ev.errors]; }