diff --git a/A00_Common/line_stack_edit.ts b/A00_Common/line_stack_edit.ts index 0dfc1c650..f988673a6 100644 --- a/A00_Common/line_stack_edit.ts +++ b/A00_Common/line_stack_edit.ts @@ -47,7 +47,7 @@ import { tableAlias, unswap, } from "../common_util/common_util_line_stack_pick"; -import { varUnit } from "../common_util/common_util_line_stack_unit"; +import { shownUnitOf, type TableLook } from "../common_util/common_util_line_stack_unit"; import { createUnitPick, NO_UNIT } from "../M02_MasterTemplete/DataFlow/M02_DF_UnitPick"; import { reach } from "./formula_doc_render"; import { attachDrag } from "./line_stack_drag"; @@ -80,6 +80,8 @@ export interface EditHooks { calc?: (doc: FormulaDoc, given: Record) => DocAnswer; /** 842-3 그 기계 줄 인력 · 잡품 목록(기본값 바탕) */ lists?: (given: Record) => Record | null; + /** 845-2 표 한 칸 읽기(캐시) — 「줄 따름」 값의 고른 줄 단위 */ + look?: TableLook; } const field = ( @@ -325,8 +327,10 @@ function qtyBox( // 838-3 공식 · 표 값 묶음에 이미 보이는 기호 = 줄 쪽 표 칩 걷음(묶음에서 고름) · 묶음에 없으면 줄에서 고름 const inBlock = h.items.some((it) => it.줄.some((l) => l.꼴 === "공식" && l.기호.includes(text))); const tag = v && !inBlock ? sourceTag(h, text, v.연결) : null; - if (tag && isNum(val)) - tag.title = `${eqText(val)}${varUnit(v) ? ` ${varUnit(v)}` : ""} — ${tag.title || tag.textContent}`; + if (tag && isNum(val)) { + const u = shownUnitOf(v, (h.answer.값 ?? {}) as Record, h.look); + tag.title = `${eqText(val)}${u ? ` ${u}` : ""} — ${tag.title || tag.textContent}`; + } return el("span", { className: "lstk-qty", children: tag ? [f, tag] : [f] }); } diff --git a/A00_Common/line_stack_out.ts b/A00_Common/line_stack_out.ts index b1649a7e5..9544e7a34 100644 --- a/A00_Common/line_stack_out.ts +++ b/A00_Common/line_stack_out.ts @@ -48,10 +48,10 @@ import { machineUnit, moneyUnits, unitText, - varUnit, + shownUnitOf, } from "../common_util/common_util_line_stack_unit"; import { partName } from "../common_util/common_util_machine_symbol_name"; -import { katexNode, symTex, tex, valTex, type TexCtx } from "./line_stack_tex"; +import { katexNode, symTex, tex, txt, valTex, type TexCtx } from "./line_stack_tex"; import { lt } from "./line_stack_text"; type Cols = Partial>; @@ -127,7 +127,8 @@ function rows( const shown = new Set(reach(doc, vals)); // 지금 탄 갈래로 닿는 기호만 const steps = lineMap(doc); const place = placeNames(doc, stackOf(doc)); // 번지 읽는 이름(804-5) - const unit = (id: string): string => varUnit(vars.get(id)); // 845-2 단위 규칙 한 벌 + const look = (p: Record): unknown => h.cache.links.table(p); + const unit = (id: string): string => shownUnitOf(vars.get(id), vals, look); // 845-2 단위 규칙 한 벌(줄 따름 = 고른 표 줄 단위) const alias = tableAlias(doc); // 표 값 기호 화면 이름(요구 683) const machine = doc.부품?.갈래 === "기계경비식"; // 845-2 한 줄 분수 한 층(중간 줄로 나눔) const taken = new Set([...vars.keys(), ...lines.keys(), ...Object.values(alias)]); @@ -144,7 +145,7 @@ function rows( more: Partial, ): (string | HTMLElement)[] => { const res = isNum(v) - ? katexNode(`${code(id) ? String(v) : valTex(v)}\\ ${u ? `\\text{${u}}` : ""}`) + ? katexNode(`${code(id) ? String(v) : valTex(v)}\\ ${u ? txt(u) : ""}`) // 단위 글 = TeX 안전(「%」 주석 · 「^」 첨자 막음) : typeof v === "string" // 글 값(기계 원문번호 등 · 838-3) = 그 글 · 기계면 이름 ? el("span", { text: machineText(v, () => h.redraw?.()) ?? v }) : missing(a, id); diff --git a/A00_Common/line_stack_rows.ts b/A00_Common/line_stack_rows.ts index 0973c6e8f..96cb3676c 100644 --- a/A00_Common/line_stack_rows.ts +++ b/A00_Common/line_stack_rows.ts @@ -44,7 +44,7 @@ import { pickCell, tableAlias, } from "../common_util/common_util_line_stack_pick"; -import { unitText, varUnit } from "../common_util/common_util_line_stack_unit"; +import { shownUnitOf, unitText } from "../common_util/common_util_line_stack_unit"; import { openFindPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Find_Pick"; import type { EditHooks } from "./line_stack_edit"; import { openTablePick } from "./line_stack_pick"; @@ -157,14 +157,16 @@ export function buildBlock( const show = (id: string): string => alias[id] ?? id; const shownText = (t: string): string => aliasText(t, alias); const realText = (t: string): string => aliasText(t, alias, true); + /** 845-2 단위 한 벌 — 변수 단위 › 기계 칸 › 고른 표 줄 단위(줄 따름) */ + const unitOf = (id: string): string => shownUnitOf(vars.get(id), vals, h.look); const valText = (id: string): string => { const v = vals[id]; const mach = machineText(v, () => h.change(() => undefined)); // 838-3 기계 원문번호 = 이름 if (mach) return mach; - if (isNum(v) || typeof v === "string") return `${eqText(v)} ${varUnit(vars.get(id))}`.trim(); + if (isNum(v) || typeof v === "string") return `${eqText(v)} ${unitOf(id)}`.trim(); if (v && typeof v === "object") { const key = vars.get(id)?.연결?.키; // 845-2 묶음 값 = 그 문서 결과 단위(원/시간 등) - const u = varUnit(vars.get(id)) || unitText(key ? h.subDoc(key)?.결과단위 : ""); + const u = unitOf(id) || unitText(key ? h.subDoc(key)?.결과단위 : ""); return `${["노무비", "재료비", "경비"].map((c) => `${c} ${eqText((v as Record)[c])}`).join(" · ")} ${u}`.trim(); // prettier-ignore } return `— ${h.answer.빈값?.[id] ?? ""}`.trim(); @@ -340,7 +342,9 @@ export function buildBlock( ...(expr ? [tap(el("span", { className: "lstk-expr", text: shownText(expr) }), id)] : []), // 결정된 식이 수 하나면(만약이 수를 탄 줄) 값을 또 적지 않음 ...(expr && iffy && tryParse(expr)?.[0] === "num" - ? [] + ? unitOf(id) // 수 하나 = 값 다시 안 적고 단위만(845-2) + ? [el("span", { className: "lstk-unit", text: unitOf(id) })] + : [] : [...(expr ? ["="] : []), el("span", { className: "lstk-val", text: valText(id) })]), ]; else @@ -350,7 +354,7 @@ export function buildBlock( input(shownText(valueOf), valText(id), "lstk-f-m", (t) => h.change((doc) => setRow(doc, id, realText(t))), ), - ...(varUnit(v) ? [el("span", { className: "lstk-unit", text: varUnit(v) })] : []), + ...(unitOf(id) ? [el("span", { className: "lstk-unit", text: unitOf(id) })] : []), ]; const why = iffy ? whyText(tree!, vals, { diff --git a/A00_Common/line_stack_screen.ts b/A00_Common/line_stack_screen.ts index b356aa72b..c54f1fdb3 100644 --- a/A00_Common/line_stack_screen.ts +++ b/A00_Common/line_stack_screen.ts @@ -377,6 +377,7 @@ export function createStackScreen(opt: StackScreenOptions) { machine: machineCtx(), calc: calcSub, lists: (g) => listsOf((q) => cache.links.element(q), g), + look: (p) => cache.links.table(p), before: saved ? lineMap(JSON.parse(saved) as StackDoc) : undefined, }), ] diff --git a/A00_Common/line_stack_tex.ts b/A00_Common/line_stack_tex.ts index 9e9a64901..78d01d1a3 100644 --- a/A00_Common/line_stack_tex.ts +++ b/A00_Common/line_stack_tex.ts @@ -61,7 +61,7 @@ const PREC: Record = { const prec = (n: Node): number => (n[0] === "bin" ? PREC[n[1]] : n[0] === "neg" ? 6 : 9); /** TeX 글 안전 — \text{} 안 */ -const txt = (s: string): string => `\\text{${s.replace(/[\\{}$&#^_%~]/g, (c) => `\\${c}`)}}`; +export const txt = (s: string): string => `\\text{${s.replace(/[\\{}$&#^_%~]/g, (c) => `\\${c}`)}}`; const word = (s: string): string => /^[A-Za-z]$/.test(s) ? s : /^[A-Za-z]+$/.test(s) ? `\\mathit{${s}}` : txt(s); diff --git a/common_util/common_util_line_stack_unit.ts b/common_util/common_util_line_stack_unit.ts index 78ab2a8ee..e2b070470 100644 --- a/common_util/common_util_line_stack_unit.ts +++ b/common_util/common_util_line_stack_unit.ts @@ -5,14 +5,16 @@ * ㉢ 비목 · 금액 줄 = 「단가 원/단위 × (수량 식) 단위 = 금액 원」(`moneyUnits` — 수량이 식이면 괄호로 식 전체에 단위) · * 소계 · 합계 = 원(머리 [원/㎥] = 결과 하나당) * ㉣ 무차원 = 빈 칸 ㉤ 율 = %(「%(인력품 대비)」 등 설명 괄호 뗌) ㉥ 왼쪽 값 칸 = 칸 바로 뒤 한 자리 - * 메타 단위(「구분 따름」 · 「줄 따름」 · 「수(…)」 · 「원문 단위 없음」) = 단위 아님 → 안 그림 · 「10^-7」 = 10⁻⁷ + * 메타 단위(「구분 따름」 · 「줄 따름」 · 「수(…)」 · 「원문 단위 없음」) = 단위 아님 → 안 그림 · 「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 와 같은 글) */ +/** 기계 표 칸 단위(`build_기계_건설품셈.py` UNIT · `M02_MyMachine.py` UNITS · 845-1 문서 단위와 같은 글) */ export const MACHINE_UNIT: Record = { 가격: "천원", 내용시간: "시간", @@ -22,7 +24,7 @@ export const MACHINE_UNIT: Record = { 관리비계수: "10^-7/시간", 손료계수: "10^-7/시간", 주연료: "ℓ/hr", - 전력: "kWh/hr", + 전력: "㎾h/hr", 잡재료율: "%", 잡품: "%", 조종원: "인/일", @@ -39,9 +41,17 @@ 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⁻⁷"); + 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]); @@ -53,6 +63,29 @@ export function varUnit(v: DocVar | undefined): string { 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 { @@ -83,6 +116,8 @@ export function moneyUnits( const q = has ? unitText(line.단위) : ""; const wrap = !!q && !!qty && !isAtom(qty); const own = [sub?.결과단위, priceVar?.단위].map(unitText).find((u) => u.startsWith("원")); - const p = has && isAtom(price) ? (own ?? (q ? `${AMOUNT_UNIT}/${q}` : "")) : ""; + // 단가 분모가 수량 단위와 같은 뜻이면 수량 단위 글로(「원/일 × 인」 대신 「원/인 × 인」) + 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 }; } diff --git a/resources/tester/helper_845_2_unit_show.cjs b/resources/tester/helper_845_2_unit_show.cjs index 38345a567..9754c146d 100644 --- a/resources/tester/helper_845_2_unit_show.cjs +++ b/resources/tester/helper_845_2_unit_show.cjs @@ -278,7 +278,7 @@ function left(c) { const root = edit.buildEdit({ doc, answer: a, items: ls.stackOf(doc), change: noop, table: async () => null, subDoc: sub, name: () => "", open: new Set((doc.금액 || []).map((_, i) => `mctl:${i}`)), pair: noop, - machine: { scope: "x" }, calc: (d, g) => calc.calcDoc(d, cache.links, g, [], new Map(), {}), lists: () => null, + machine: { scope: "x" }, calc: (d, g) => calc.calcDoc(d, cache.links, g, [], new Map(), {}), lists: () => null, look: (p) => cache.links.table(p), }); // prettier-ignore const rows = []; const walk = (n) => { diff --git a/resources/tester/test_845_2_unit_show.py b/resources/tester/test_845_2_unit_show.py index 3c5615316..c542054d1 100644 --- a/resources/tester/test_845_2_unit_show.py +++ b/resources/tester/test_845_2_unit_show.py @@ -56,7 +56,9 @@ def world(): } ) names = [c["문서"].get("이름") for c in cases if c["기계"]] - got = _js({"할일": "전수", "문서들": cases, "보기": ["GC000088"], "기계이름": names}) + got = _js( + {"할일": "전수", "문서들": cases, "보기": ["GC000088", "GC000006"], "기계이름": names} + ) return {g["키"]: g for g in got} @@ -70,20 +72,35 @@ def test_전수_규칙_어긋남_0(world): def test_GC000088_비목_줄_단위_자리(world): rows = [r["t"] for r in world["GC000088"]["오른쪽"]] labor = next(t for t in rows if t.startswith("노무비 : ") and "×" in t) - assert labor.startswith("노무비 : 59,020 원/시간 × ⟦\\left(1 \\times 8 \\div 560\\right)⟧ 시간 = ") + assert labor.startswith( + "노무비 : 59,020 원/시간 × ⟦\\left(1 \\times 8 \\div 560\\right)⟧ 시간 = " + ) assert labor.endswith(" 원") assert any(t.startswith("소 계 : ") and t.endswith(" 원") for t in rows) assert any(t.startswith("계 : ") and t.endswith(" 원") for t in rows) # 펼친 굴착기 시간당 경비 — 겹분수 대신 중간 줄 sub = "\n".join(rows) - assert "⟦\\text{상여}⟧ = ⟦\\dfrac{\\text{상여분자}}{\\text{상여분모}}⟧ = ⟦\\dfrac{16}{12}⟧" in sub - assert "⟦\\text{휴지}⟧ = ⟦\\dfrac{\\text{휴지분자}}{\\text{휴지분모}}⟧ = ⟦\\dfrac{25}{20}⟧" in sub + assert ( + "⟦\\text{상여}⟧ = ⟦\\dfrac{\\text{상여분자}}{\\text{상여분모}}⟧ = ⟦\\dfrac{16}{12}⟧" in sub + ) + assert ( + "⟦\\text{휴지}⟧ = ⟦\\dfrac{\\text{휴지분자}}{\\text{휴지분모}}⟧ = ⟦\\dfrac{25}{20}⟧" in sub + ) assert "\\dfrac{1}{8} \\times 1.333333 \\times 1.25⟧ = ⟦0.208333" in sub assert "⟦\\text{손료계수}⟧ = " in sub and "= ⟦2{,}085\\ \\text{10⁻⁷/시간}⟧" in sub assert "\\dfrac{142{,}069 \\times 1{,}000 \\times 2{,}085}{10{,}000{,}000}⟧ = ⟦29{,}621" in sub assert "⟦\\text{가격}⟧ = 142,069 천원" in sub assert "⟦\\text{직종}⟧ = 1048" in sub # 코드 값 = 쉼표 없음 - assert "조종원 : ⟦283{,}297⟧ 원/인 × ⟦\\left(1 \\times 0.208333\\right)⟧ 인 = 59,020.2 원" in sub + assert ( + "조종원 : ⟦283{,}297⟧ 원/인 × ⟦\\left(1 \\times 0.208333\\right)⟧ 인 = 59,020.2 원" in sub + ) + assert "⟦\\text{가산}⟧ = ⟦0\\ \\text{\\%}⟧" in sub # % = TeX 글(주석으로 안 먹힘) + + +def test_줄_따름_고른_표_줄_단위(world): + rows = "\n".join(r["t"] for r in world["GC000006"]["오른쪽"]) + assert "⟦Q_{\\text{비계공2}}⟧ = 3 인" in rows # 변수 단위 「줄 따름」 = 고른 줄(비계공) 단위 칸 + assert "비계공 : ⟦281{,}939⟧ 원/인 × ⟦0.03⟧ 인 = 8,458.1 원" in rows # 노임 원/일 = 인 하루 값 def test_왼쪽_제어_구역_단위_오른쪽과_같은_글(world): @@ -98,23 +115,40 @@ def test_단위_규칙_낱개(): got = _js( { "할일": "단위", - "글": ["구분 따름", "줄 따름", "수(단위는 줄의 단위 칸 따름)", "수", "원문 단위 없음", - "%(주연료비의)", "10^-7/시간", "㎥/hr", "인", ""], # fmt: skip + "글": [ + "구분 따름", + "줄 따름", + "수(단위는 줄의 단위 칸 따름)", + "수", + "원문 단위 없음", + "%(주연료비의)", + "10^-7/시간", + "㎥/hr", + "인", + "", + ], # fmt: skip "변수": [ {"단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}}, {"단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "잡재료율"}}, {"단위": "%", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}}, {"단위": "", "연결": {"갈래": "요소", "키": "LB:{S_직종}"}}, + {"단위": "kWh/hr"}, ], "금액": [ - [{"단위": "시간", "수량": "Q × 8 ÷ Q2"}, ["var", "P"], ["bin", "/", ["var", "a"], ["var", "b"]], {"단위": ""}, {"결과단위": "원/시간"}], # fmt: skip - [{"단위": "인", "수량": "Q"}, ["var", "w"], ["var", "Q"], {"단위": ""}, None], + [ + {"단위": "시간", "수량": "Q × 8 ÷ Q2"}, + ["var", "P"], + ["bin", "/", ["var", "a"], ["var", "b"]], + {"단위": ""}, + {"결과단위": "원/시간"}, + ], # fmt: skip + [{"단위": "인", "수량": "Q"}, ["var", "w"], ["var", "Q"], {"단위": "원/일"}, None], [{"단위": "식"}, ["bin", "*", ["var", "a"], ["var", "b"]], None, None, None], ], } ) assert got["글"] == ["", "", "", "", "", "%", "10⁻⁷/시간", "㎥/hr", "인", ""] - assert got["변수"] == ["천원", "%", "%", ""] + assert got["변수"] == ["천원", "%", "%", "", "㎾h/hr"] assert got["금액"] == [ {"price": "원/시간", "qty": "시간", "wrap": True}, {"price": "원/인", "qty": "인", "wrap": False},