/* ============================================================================= * common_util_line_stack_work.ts * 시간당 작업량 그리기 자료(PLAN 846-2 · 요구 881 · 882 · 883) — 산출근거 · 기계 제어 구역 · 기계 보기가 같이 씀 * 셈 답 기계 줄 `줄[i].작업량`(846-1 계산기 `common_util_formula_doc_work`) → 그림 자료 * ㉠ 일당 풀이 = 율 식 「D ÷ (8 × 대수…)」 → 시공량 D · 나눔 항(8 = 시간 · 그 밖 = 대수) * ㉡ 비목 줄 = 수량 식의 작업량 자리를 바꾼 글(`swap` — 「나머지 ÷ Q_NEW」) → 나머지(1 이면 없음) · * 꼴이 다르면 null(지금 꼴 그대로) * ㉢ 단위 — 작업량 「㎥/hr」 → 「㎥/시간」 · 시공량 = 「㎥/일」 * ㉣ 시간당 식 풀이 = 작업량 틀 결과 식이 읽는 기호(q · K · f · E · cm — 품셈 기호 그대로) * 그리기만 — 식 · 값 · 금액 불변 · 화면 없음(시험이 Node 로 돌림) * ========================================================================== */ import { names, type FormulaDoc, type Node } from "./common_util_formula_doc"; import type { DocAnswer } from "./common_util_formula_doc_calc"; import { Q_NEW, specsOf, swap, WORK, type WorkInfo, type WorkSpec, } from "./common_util_formula_doc_work"; import { nodeOf } from "./common_util_line_stack"; import { unitText } from "./common_util_line_stack_unit"; /** 셈 답 i 번째 금액 줄의 작업량(없으면 null) */ export function workAt(a: DocAnswer | null | undefined, i: number): WorkInfo | null { const got = (a?.줄?.[i] as Record | undefined)?.[WORK]; return got && typeof got === "object" ? (got as WorkInfo) : null; } /** 그 기계의 첫 작업량 줄(같은 기계 두 줄 = 덮음 하나) */ export function workOfMachine(a: DocAnswer | null | undefined, machine: string): WorkInfo | null { for (let i = 0; i < (a?.줄?.length ?? 0); i++) { const w = workAt(a, i); if (w && w.기계 === machine) return w; } return null; } /** 기계 작업량 줄(기계 값 칸 `작업량` — 캐시 `links.element`) · 없음 · 못 읽음 = 빈 목록 */ export function machineWork( element: (p: Record) => unknown, machine: string, ): WorkSpec[] { try { return specsOf(element({ 키: `EQ:${machine}`, 칸: WORK })); } catch { return []; } } /** 작업량 단위 → 화면(「/hr」 = 「/시간」) */ export const workUnit = (u: unknown): string => unitText(u).replace(/\/\s*(hr|h|Hr|HR|시간)$/, "/시간"); /** 시공량(하루) 단위 — 작업량 단위 분자 + 「/일」 */ export const dayUnit = (u: unknown): string => { const top = workUnit(u).replace(/\/시간$/, ""); return top ? `${top}/일` : ""; }; const factors = (n: Node): Node[] => n[0] === "bin" && n[1] === "*" ? [...factors(n[2]), ...factors(n[3])] : [n]; export interface DailyParts { /** 하루 시공량 식 */ day: Node; /** 나눔 항 — 8 = 시간 · 그 밖 = 대수 */ per: { node: Node; hours: boolean }[]; } /** ㉠ 일당 율 식 「D ÷ (8 × …)」 → 시공량 · 나눔 항 · 꼴이 다르면 null */ export function dailyParts(rate: unknown): DailyParts | null { const n = nodeOf(rate); if (!n || n[0] !== "bin" || n[1] !== "/") return null; const per = factors(n[3]).map((x) => ({ node: x, hours: x[0] === "num" && x[1] === 8 })); if (!per.some((p) => p.hours)) return null; // 대수 먼저 · 8 시간 끝(「1 대 × 8 시간」 — 경쟁사 「대수 × 8hr」 꼴) return { day: n[2], per: [...per.filter((p) => !p.hours), ...per.filter((p) => p.hours)] }; } /** ㉡ 수량 식 → 「나머지 ÷ Q」 의 나머지(없으면 rest null) · 꼴이 다르면 null */ export function restOfQty(doc: FormulaDoc, qty: unknown): { rest: Node | null } | null { let text: string; try { text = swap(doc, qty); } catch { return null; } const n = nodeOf(text); if (!n || n[0] !== "bin" || n[1] !== "/" || n[3][0] !== "var" || n[3][1] !== Q_NEW) return null; const rest = n[2]; return { rest: rest[0] === "num" && rest[1] === 1 ? null : rest }; } /** ㉣ 작업량 틀 결과 식 · 그 식이 읽는 기호(차례 그대로 · 겹침 뺌) */ export function hourlyParts(tpl: FormulaDoc): { id: string; node: Node; uses: string[] } | null { const id = String(tpl.결과 ?? ""); const line = (tpl.식 ?? []).find((l) => String(l.기호 ?? "") === id); const node = line ? nodeOf(line.식) : null; return node ? { id, node, uses: [...new Set(names(node))] } : null; }