- 재료 요소 mode(fixed · choice · rule) · scope · rule{field · op · from} · default · slot
- 입력 갈래 logic + CfLogicRef{key · name · outputs · designs}
- 선 규칙 CfLink.rule — lookup · gate · scale · round · CfRound(func.output.round · output.round)
- 흐름 CfStep · CfDesign · traceBack · traceForward · designInputs · Eval CfBreak · breaks
- M02_DF_Rule.ts — ruleLabel · applyRule · applyRound (속은 준비 중)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
233 lines
10 KiB
TypeScript
233 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* M02_DF_Eval.ts
|
|
* 단가산출 다이어그램 계산 · 검사 — 함수 상자는 차례대로 시트를 우리 스프레드시트 엔진으로 셈
|
|
* (받는 칸에 앞 값을 넣음) · 출력 상자를 비용 종류별로 모아 원가 · DOM 없음
|
|
* 엔진을 정적으로 가져옴 — 화면(Entry)은 DataFlow 를 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_DF_Model";
|
|
|
|
/** 알약 값 — 수 · 글 · 참거짓 · 빈 값 · 표(머리 줄 포함 칸 덩어리) */
|
|
export type CfValue = number | string | boolean | null | (number | string | null)[][];
|
|
|
|
/** 입력 값 묶음 — 상자 id → 알약 이름 → 값(입력 상자 요소 값 · 표 · 변수 값 · 함수 상자 변수 값) */
|
|
export type CfValues = Record<string, Record<string, CfValue>>;
|
|
|
|
type Scalar = number | string | boolean | null;
|
|
|
|
export interface CfEvalResult {
|
|
/** 상자 id → 내보내는 알약 이름 → 값 */
|
|
ports: Record<string, Record<string, CfValue>>;
|
|
/** 출력 상자 id → 값 */
|
|
outputs: Record<string, Scalar>;
|
|
cost: {
|
|
노무비: number;
|
|
재료비: number;
|
|
경비: number;
|
|
/** 비용 아닌 출력(값) — 출력 상자 이름 → 값 */
|
|
값들: Record<string, Scalar>;
|
|
계: 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, string> = {}): 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<string, Cell>, 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<string, Cell>, 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<string, CfValue> {
|
|
const given = values[box.id] ?? {};
|
|
const pills = portsOut(box);
|
|
const out: Record<string, CfValue> = {};
|
|
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<string, CfValue>,
|
|
given: Record<string, CfValue>,
|
|
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, 열: "DF", 서식: [{}], 시트: [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<string, CfValue> =>
|
|
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];
|
|
}
|
|
|
|
/** 끊긴 곳 하나 — 상자 · 알약 · 선 위에 표시할 것 */
|
|
export interface CfBreak {
|
|
kind:
|
|
| "빈 받는 자리"
|
|
| "통과 안 됨"
|
|
| "표에 없는 조건"
|
|
| "맞는 재료 없음"
|
|
| "조건 겹침"
|
|
| "조건 안 맞음"
|
|
| "오류";
|
|
box: string;
|
|
port?: string;
|
|
link?: string;
|
|
/** 쉬운 말 */
|
|
text: string;
|
|
}
|
|
|
|
/** 끊긴 곳 목록(넓힘 · 준비 중) */
|
|
export function breaks(_lg: CfLogic, _values: CfValues = {}): CfBreak[] {
|
|
throw new Error("준비 중");
|
|
}
|