Files
Aislo/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts
T

255 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M02_DF_Detail.ts
* 상세 내용 판(선 · 알약) 공용 — 값 읽기 · 규칙 미리 보기 · 겹침/안 맞음 · 흐름 줄 · 끊김 까닭 · DOM 없음
* 모델의 traceBack · traceForward · breaks · applyRule · ruleLabel 이 아직 준비 중이면 같은 뜻의 작은 셈으로 메움
* 글 [한, 영] 짝은 아래 TEXT
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { breaks, evalLogic, type CfEvalResult, type CfValue, type CfValues } from "./M02_DF_Eval";
import {
portsIn,
portsOut,
traceBack,
traceForward,
type CfBox,
type CfGateOp,
type CfLink,
type CfLogic,
type CfPort,
type CfRound,
type CfRule,
type CfStep,
} from "./M02_DF_Model";
import { applyRule, ruleLabel } from "./M02_DF_Rule";
const TEXT = {
Table: ["표 {n}줄", "table, {n} rows"],
Empty: ["값 없음", "no value"],
LabelLookup: ["표에서 「{col}」 찾기", "Look up “{col}” in the table"],
LabelGate: ["「{pill}」 {op} {value} 일 때만 보냄", "Send only when “{pill}” {op} {value}"],
LabelScale: ["값 {op} {value}", "value {op} {value}"],
LabelRound: ["소수 {places}자리 {method}", "{places} decimals, {method}"],
NoRule: ["규칙 없음 — 값이 그대로 감", "No rule — the value goes as is"],
Blocked: ["통과 안 됨", "does not pass"],
NoRowLater: ["표에서 맞는 줄은 계산 뒤에 보임", "The matching row shows after calculation"],
EmptyIn: ["빈 받는 자리 — 선이 없음", "Empty receiving spot — no line"],
NoCell: ["받은 알약을 칸에 놓지 않음", "The received pill is not placed in a cell"],
Dead: ["아직 어디로도 안 감", "Goes nowhere yet"],
NoValue: ["값이 아직 없음", "No value yet"],
} as const;
export const td = (key: keyof typeof TEXT, fill: Record<string, string | number> = {}): string =>
Object.entries(fill).reduce(
(s, [k, v]) => s.replace(`{${k}}`, String(v)),
TEXT[key][currentLanguageIndex === 1 ? 1 : 0] as string,
);
export const GATE_OPS: CfGateOp[] = ["=", "≠", "≥", "≤", ">", "<"];
export const SCALE_OPS: ("×" | "÷" | "+%" | "-%")[] = ["×", "÷", "+%", "-%"];
export const boxOf = (lg: CfLogic, id: string): CfBox | undefined =>
lg.boxes.find((b) => b.id === id);
export const portText = (lg: CfLogic, p: CfPort): string =>
`${boxOf(lg, p.box)?.name ?? "?"} › ${p.name}`;
/** 계산 한 번 — 못 하면 null */
export function evalSafe(lg: CfLogic, values?: CfValues): CfEvalResult | null {
try {
return evalLogic(lg, values);
} catch {
return null;
}
}
export const valueOf = (ev: CfEvalResult | null, p: CfPort): CfValue | undefined =>
ev?.ports[p.box]?.[p.name];
/** 화면에 보일 값 글 — 표는 줄 수 */
export function showValue(v: CfValue | undefined): string {
if (v === undefined || v === null) return td("Empty");
if (Array.isArray(v)) return td("Table", { n: Math.max(0, v.length - 1) });
return typeof v === "number" ? String(Number(v.toPrecision(12))) : String(v);
}
export function unitOf(lg: CfLogic, p: CfPort): string {
const box = boxOf(lg, p.box);
return (box && portsOut(box).find((x) => x.name === p.name)?.unit) || box?.unit || "-";
}
/** 알약이 나가는(out) 쪽인가 */
export const isOut = (box: CfBox, name: string): boolean =>
portsOut(box).some((p) => p.name === name);
/* ── 규칙 ─────────────────────────────────────────────────────────────── */
const num = (v: unknown): number | null => {
const n = typeof v === "number" ? v : Number(v);
return Number.isFinite(n) && v !== "" && v !== null ? n : null;
};
/** 조건 통과 — 숫자면 숫자끼리 · 아니면 글끼리 */
export function gatePass(op: CfGateOp, v: unknown, value: number | string): boolean {
const a = num(v);
const b = num(value);
const [x, y] = a !== null && b !== null ? [a, b] : [String(v ?? ""), String(value)];
switch (op) {
case "=":
return x === y;
case "≠":
return x !== y;
case "≥":
return x >= y;
case "≤":
return x <= y;
case ">":
return x > y;
default:
return x < y;
}
}
export function roundValue(r: CfRound, v: number): number {
const k = 10 ** r.places;
const f = r.method === "올림" ? Math.ceil : r.method === "버림" ? Math.floor : Math.round;
return f(v * k) / k;
}
/** 선 위 규칙 글 — 모델이 준비되면 그것, 아니면 같은 뜻의 글 */
export function labelOf(lg: CfLogic, rule: CfRule | undefined): string {
if (!rule) return td("NoRule");
try {
return ruleLabel(rule);
} catch {
if (rule.kind === "lookup") return td("LabelLookup", { col: rule.col });
if (rule.kind === "gate")
return td("LabelGate", { pill: portText(lg, rule.from), op: rule.op, value: rule.value });
if (rule.kind === "scale") return td("LabelScale", { op: rule.op, value: rule.value });
return td("LabelRound", { places: rule.places, method: rule.method });
}
}
/** 지나가기 전 값 → 지난 뒤 값 미리 보기 */
export function preview(
link: CfLink,
rule: CfRule | undefined,
ev: CfEvalResult | null,
): { before: string; after: string } {
const raw = valueOf(ev, link.from);
const before = showValue(raw);
if (!rule) return { before, after: before };
const conds: Record<string, unknown> = {};
if (rule.kind === "lookup") for (const c of rule.conds) conds[c.col] = valueOf(ev, c.from);
try {
const r = applyRule(rule, raw, conds);
return {
before,
after: r.value === null ? (r.why ?? td("Empty")) : showValue(r.value as CfValue),
};
} catch {
const n = num(raw);
if (rule.kind === "gate")
return {
before,
after: gatePass(rule.op, valueOf(ev, rule.from), rule.value) ? before : td("Blocked"),
};
if (rule.kind === "lookup" || n === null) return { before, after: td("NoRowLater") };
if (rule.kind === "round") return { before, after: showValue(roundValue(rule, n)) };
const v = rule.value;
const out =
rule.op === "×"
? n * v
: rule.op === "÷"
? v
? n / v
: NaN
: rule.op === "+%"
? n * (1 + v / 100)
: n * (1 - v / 100);
return { before, after: Number.isFinite(out) ? showValue(out) : td("Empty") };
}
}
/** 같은 알약에서 나간 조건 선 — 지금 값으로 통과하는 수 */
export function gateReport(
lg: CfLogic,
link: CfLink,
ev: CfEvalResult | null,
): { total: number; pass: string[] } {
const same = lg.links.filter(
(l) =>
l.from.box === link.from.box && l.from.name === link.from.name && l.rule?.kind === "gate",
);
const pass = same
.filter((l) => {
const r = l.rule as Extract<CfRule, { kind: "gate" }>;
return gatePass(r.op, valueOf(ev, r.from), r.value);
})
.map((l) => l.id);
return { total: same.length, pass };
}
/* ── 흐름 · 끊김 ─────────────────────────────────────────────────────── */
/** 거슬러 오르는 길(dir "in" = 온 길) · 따라가는 길(dir "out") — 모델 것, 없으면 작은 셈 */
export function trace(lg: CfLogic, p: CfPort, dir: "in" | "out"): CfStep[] {
try {
return dir === "in" ? traceBack(lg, p) : traceForward(lg, p);
} catch {
const out: CfStep[] = [];
const seen = new Set<string>();
const walk = (port: CfPort, back: boolean): void => {
const key = `${port.box}/${port.name}`;
if (seen.has(key)) return;
seen.add(key);
const box = boxOf(lg, port.box);
if (!box) return;
if (back) {
const into = lg.links.filter(
(l) => l.to.box === port.box && (isOut(box, port.name) || l.to.name === port.name),
);
for (const l of into) {
out.push({ box: l.from.box, port: l.from.name, dir: "out", link: l.id, rule: l.rule });
walk(l.from, true);
}
} else {
const outs = lg.links.filter((l) => l.from.box === port.box && l.from.name === port.name);
for (const l of outs) {
out.push({ box: l.to.box, port: l.to.name, dir: "in", link: l.id, rule: l.rule });
const next = boxOf(lg, l.to.box);
for (const o of next ? portsOut(next) : []) walk({ box: next!.id, name: o.name }, false);
}
}
};
walk(p, dir === "in");
return out;
}
}
/** 그 상자(알약)에서 끊긴 곳 — 쉬운 말 */
export function breakLines(lg: CfLogic, boxId: string, name?: string): string[] {
try {
return breaks(lg)
.filter((b) => b.box === boxId && (!name || !b.port || b.port === name))
.map((b) => b.text);
} catch {
// 모델의 끊긴 곳 목록이 준비되기 전 — 같은 뜻의 작은 셈
}
const box = boxOf(lg, boxId);
if (!box) return [];
const out: string[] = [];
for (const p of portsIn(box)) {
if (name && p.name !== name) continue;
if (!lg.links.some((l) => l.to.box === boxId && l.to.name === p.name)) out.push(td("EmptyIn"));
else if (box.func?.inputs.some((i) => i.port === p.name && !i.cell)) out.push(td("NoCell"));
}
for (const p of portsOut(box)) {
if (name && p.name !== name) continue;
if (!lg.links.some((l) => l.from.box === boxId && l.from.name === p.name)) out.push(td("Dead"));
}
return out;
}
export const noValue = (): string => td("NoValue");