/* ============================================================================= * common_util_line_stack_unit.ts * 단가산출 단위 그리는 규칙 한 벌(PLAN 845-2 · 요구 880) — 왼쪽 편집 · 오른쪽 산출근거 · 기계 제어 구역 · 기계 보기가 같이 씀 * ㉠ 값 줄 = 값 뒤 단위(`varUnit`) ㉡ 식 줄 = 결과 값 뒤만(식 가운데 숫자 뒤 없음) * ㉢ 비목 · 금액 줄 = 「단가 원/단위 × (수량 식) 단위 = 금액 원」(`moneyUnits` — 수량이 식이면 괄호로 식 전체에 단위) · * 소계 · 합계 = 원(머리 [원/㎥] = 결과 하나당) * ㉣ 무차원 = 빈 칸 ㉤ 율 = %(「%(인력품 대비)」 등 설명 괄호 뗌) ㉥ 왼쪽 값 칸 = 칸 바로 뒤 한 자리 * 메타 단위(「구분 따름」 · 「줄 따름」 · 「수(…)」 · 「원문 단위 없음」) = 단위 아님 → 안 그림 · 「10^-7」 = 10⁻⁷ · kWh = ㎾h * 「줄 따름」 표 값 = 지금 조건으로 고른 표 줄의 「단위」 칸(`rowUnit`) · 단가 분모 = 같은 뜻이면 수량 단위 글(원/일 × 인 → 원/인) * 기계 칸 연결(요소 `EQ:` 칸) 변수 = 변수 단위가 빈 칸이면 기계 표 칸 단위(`MACHINE_UNIT` — 기계 표 빌더 · 내 기계와 같은 글) * 그리기만 — 자료 · 식 · 금액 불변 · 화면 없음(시험이 Node 로 돌림) * ========================================================================== */ import type { DocVar, MoneyLine, Node } from "./common_util_formula_doc"; import { evalNode, tryParse, type Values } from "./common_util_formula_doc_view"; /** 기계 표 칸 단위(`build_기계_건설품셈.py` UNIT · `M02_MyMachine.py` UNITS · 845-1 문서 단위와 같은 글) */ export const MACHINE_UNIT: Record = { 가격: "천원", 내용시간: "시간", 연간표준가동시간: "시간", 상각비계수: "10^-7/시간", 정비비계수: "10^-7/시간", 관리비계수: "10^-7/시간", 손료계수: "10^-7/시간", 주연료: "ℓ/hr", 전력: "㎾h/hr", 잡재료율: "%", 잡품: "%", 조종원: "인/일", 인력: "인/일", 노무비: "원/시간", 재료비: "원/시간", 경비: "원/시간", }; /** 금액 · 소계 · 합계 단위 */ export const AMOUNT_UNIT = "원"; /** 자료 단위 글 → 화면 단위(메타 = 빈 글) */ export function unitText(u: unknown): string { const s = String(u ?? "").trim(); if (!s || /따름|원문 단위 없음/.test(s) || /^수(\(| 또는|$)/.test(s)) return ""; return s .replace(/^%\(.*\)$/, "%") .replace(/10\^-7/g, "10⁻⁷") .replace(/kWh/g, "㎾h"); } /** 같은 뜻 다른 표기(단가 단위 분모 ↔ 수량 단위) — 노임 원/일 ↔ 인(인·일) · ㎏ ↔ kg · ℓ ↔ L */ const SAME: [string, string][] = [["일", "인"], ["㎏", "kg"], ["ℓ", "L"]]; // prettier-ignore const same = (a: string, b: string): boolean => a === b || SAME.some(([x, y]) => (a === x && b === y) || (a === y && b === x)); /** 기계 칸 이름의 화면 단위 */ export const machineUnit = (col: string): string => unitText(MACHINE_UNIT[col]); /** 변수 화면 단위 — 변수 단위 › 기계 칸 연결이면 기계 표 칸 단위 */ export function varUnit(v: DocVar | undefined): string { const own = unitText(v?.단위); if (own) return own; const k = v?.연결; return k?.갈래 === "요소" && /^EQ:/.test(String(k.키 ?? "")) && k.칸 ? machineUnit(k.칸) : ""; } /** 표 한 칸 읽기(화면 캐시 `links.table`) */ export type TableLook = (p: Record) => unknown; /** 「줄 따름」(줄마다 다른 단위) 표 값 = 지금 조건으로 고른 표 줄의 「단위」 칸 · 못 읽으면 빈 글 */ export function rowUnit(v: DocVar | undefined, vals: Values, look?: TableLook): string { const k = v?.연결; if (!look || !k?.표 || !/따름/.test(String(v?.단위 ?? ""))) return ""; const 조건: Record = {}; for (const [c, t] of Object.entries(k.조건 ?? {})) { const n = tryParse(t); if (n) 조건[c] = evalNode(n, vals); } try { return unitText(look({ 표: k.표, 칸: "단위", 조건 })); } catch { return ""; } } /** 값 줄 단위 한 벌 — 변수 단위 › 기계 칸 › 고른 표 줄 단위 */ export const shownUnitOf = (v: DocVar | undefined, vals: Values, look?: TableLook): string => varUnit(v) || rowUnit(v, vals, look); const HIDE = new Set(["반올림", "올림", "버림"]); /** 한 덩이(기호 · 수 · 칸 — 감싸개 안 한 덩이 포함) */ export function isAtom(n: Node | null | undefined): boolean { if (!n) return false; if (n[0] === "call" && HIDE.has(n[1]) && n[2].length >= 1 && n[2].length <= 2) return isAtom(n[2][0]); return n[0] === "var" || n[0] === "num" || n[0] === "field" || n[0] === "str"; } export interface MoneyUnits { /** 단가 뒤(원/시간 · 원/인 …) — 단가가 한 덩이이고 수량이 있을 때만 */ price: string; /** 수량 식 뒤 */ qty: string; /** 수량 식을 괄호로 감쌈(식이고 단위가 있을 때) */ wrap: boolean; } /** 금액 줄 단위 자리(㉢) — `priceVar` = 단가 기호 변수 · `sub` = 묶음 · 단가산출 줄이 부르는 문서 */ export function moneyUnits( line: MoneyLine, price: Node | null, qty: Node | null, priceVar?: DocVar, sub?: { 결과단위?: string } | null, ): MoneyUnits { const has = line.수량 !== undefined && line.수량 !== null && line.수량 !== ""; const q = has ? unitText(line.단위) : ""; const wrap = !!q && !!qty && !isAtom(qty); const own = [sub?.결과단위, priceVar?.단위].map(unitText).find((u) => u.startsWith("원")); // 단가 분모가 수량 단위와 같은 뜻이면 수량 단위 글로(「원/일 × 인」 대신 「원/인 × 인」) const mine = own && q && same(own.slice(2), q) ? `${AMOUNT_UNIT}/${q}` : own; const p = has && isAtom(price) ? (mine ?? (q ? `${AMOUNT_UNIT}/${q}` : "")) : ""; return { price: p, qty: q, wrap }; }