153 lines
5.8 KiB
TypeScript
153 lines
5.8 KiB
TypeScript
/* =============================================================================
|
|
* 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<string, unknown>;
|
|
|
|
/** 식 글 → 나무(못 풀면 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<string, unknown> = new Map(),
|
|
path: ReadonlySet<string> = 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<string, unknown> =>
|
|
new Map((doc.식 ?? []).map((l) => [String(l.기호 ?? ""), l.식]));
|
|
|
|
/** 선택으로 꺼진 금액 줄 — 수량 · 단가가 선택으로 꺼진 0(`offZero` · 고침 모드에서만 보임) */
|
|
export function offLine(
|
|
line: { 수량?: unknown; 단가?: unknown },
|
|
vals: Values,
|
|
lines: ReadonlyMap<string, unknown> = 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<string, number> = { 노무비: 0, 재료비: 0, 경비: 0 };
|
|
((rows ?? []) as { 비목?: Record<string, unknown>; 합밖?: 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;
|
|
}
|