From 6cf7de9e82d25e84182c9070f20d192d21751ae8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 23:29:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(B09):=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=A4=ED=8E=99=ED=84=B0=20=EB=8B=A4=EC=A7=90(9-?= =?UTF-8?q?14-2)=EB=8F=84=20=EC=84=B8=EC=9B=80=20=E2=80=94=20=EB=8B=A4?= =?UTF-8?q?=EC=A7=90=20=EC=8B=9D=20=EB=91=98=EC=A7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 노체다짐과 식이 다름 — 롤러는 V·W·E·D·f/N, 콤펙터는 A·N·H·f·E/P. 원문이 답까지 적어 두어(「∙ = 4.26 ㎥/시간」) 표 계수로 셈한 값과 맞대는 시험을 둠. - 표 칸에 단위가 붙어 오는 것(「0.09㎡」·「36,000회/hr」)을 수만 떼어 읽음. - 기종은 절 제목이 정함(플레이트 콤팩터 1.5) — 표에 기종 칸이 없음. Co-Authored-By: Claude Opus 5 (1M context) --- ...timation_MachineProductivity_Compaction.py | 92 ++++++++++++++----- resources/tester/test_b09_compaction.py | 33 +++++++ 2 files changed, 102 insertions(+), 23 deletions(-) diff --git a/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py b/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py index 29773cd0..f8d86641 100644 --- a/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py +++ b/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py @@ -1,4 +1,4 @@ -"""B09 — **다짐 공식**으로 서는 공종 (산림품셈 9-16-2 노체다짐). +"""B09 — **다짐 공식**으로 서는 공종 (산림품셈 9-16-2 노체다짐 · 9-14-2 플레이트 콤펙터). 굴착·운반과 **식이 다르다.** 굴착기는 사이클(㎝)로 세는데 다짐은 롤러가 지나간 넓이로 센다. @@ -31,15 +31,23 @@ from typing import Any _ZERO = Decimal(0) -#: 계수 이름 — 표 첫 칸이 「V(다짐속도,km/hr)」처럼 기호+설명이라 **앞 글자**로 가른다. -_KEYS = ("V", "W", "E", "D", "f", "N") -_RE_HEAD = re.compile(r"^([VWEDfN])\s*[((]") -_RE_NUMBER = re.compile(r"^-?\d+(?:\.\d+)?$") +#: 계수 이름 — 표 첫 칸이 「V(다짐속도,km/hr)」처럼 기호+설명이거나 「A」처럼 기호뿐이다. +#: ⚠ **식이 둘이다** — 롤러는 `V·W·E·D·f/N`, 플레이트 콤펙터는 `A·N·H·f·E/P` 다. +FORMULA_ROLLER = "roller" +FORMULA_PLATE = "plate" +_KEYS = { + FORMULA_ROLLER: ("V", "W", "E", "D", "f", "N"), + FORMULA_PLATE: ("A", "N", "H", "f", "E", "P"), +} +_RE_HEAD = re.compile(r"^([VWEDfNAHP])(?:[((]|$)") +#: 「0.09㎡」·「36,000회/hr」처럼 **단위가 붙어 오는 칸**이 있다 — 수만 떼어 읽는다. +_RE_NUMBER = re.compile(r"^-?[\d,]+(?:\.\d+)?") #: 다짐 공식으로 서는 공종 — 기종은 [주] 원문 그대로. #: ⚠ **롤러만** 든다(위 [주]③ 읽기). 넓히면 굴착기가 두 번 선다. COMPACTION_ITEMS: dict[str, dict[str, Any]] = { "FP-09-16-02": { + "formula": FORMULA_ROLLER, "machine_code": "1306-0100", # 진동롤러(자주식) 10ton "machine_note": "산림품셈 9-16-2 [주]① 「장비는 자주식 진동롤러(10ton)를 적용한다」", "condition_note": ( @@ -50,18 +58,31 @@ COMPACTION_ITEMS: dict[str, dict[str, Any]] = { " 뜻으로 읽음 — 이 줄에 굴착기를 또 넣으면 포설에서 센 굴착기를 두 번 셈" ), }, + "FP-09-14-02": { + "formula": FORMULA_PLATE, + "machine_code": "1730-0015", # 플레이트 콤팩터 1.5 + "machine_note": "산림품셈 9-14-2 절 제목 「다짐(플레이트 콤펙터)」 — 표에 기종 칸이 없음", + "condition_note": "구조물 되메우기 다짐에 쓰는 소형 장비 줄", + "pair_note": "", + #: ⚠ **원문이 답까지 적어 두었다** — 「∙ = 4.26 ㎥/시간」. 표 계수로 셈한 값이 이 값과 + #: 어긋나면 우리가 잘못 읽은 것이므로 **시험이 둘을 맞대 본다.** + "stated_capacity": Decimal("4.26"), + }, } def _number(cells: list[str]) -> Decimal | None: for cell in cells: text = str(cell or "").strip() - if _RE_NUMBER.match(text): - return Decimal(text) + found = _RE_NUMBER.match(text) + if found: + return Decimal(found.group(0).replace(",", "")) return None -def compaction_factors(table: dict[str, Any]) -> dict[str, Decimal] | None: +def compaction_factors( + table: dict[str, Any], formula: str = FORMULA_ROLLER +) -> dict[str, Decimal] | None: """표에서 여섯 계수를 뽑는다. 하나라도 없으면 `None` — **채워 넣지 않는다.**""" found: dict[str, Decimal] = {} for row in table.get("raw_row") or []: @@ -74,13 +95,21 @@ def compaction_factors(table: dict[str, Any]) -> dict[str, Decimal] | None: value = _number(cells[1:]) if value is not None: found.setdefault(head.group(1), value) - if set(found) != set(_KEYS): + keys = set(_KEYS[formula]) + if not keys <= set(found): return None - return found + return {key: found[key] for key in keys} -def capacity_per_hour(factors: dict[str, Decimal]) -> Decimal: - """Q = 1000 × V × W × E × D × f / N (㎥/시간).""" +def capacity_per_hour(factors: dict[str, Decimal], formula: str = FORMULA_ROLLER) -> Decimal: + """시간당 작업량(㎥/시간). **식이 둘**이라 어느 식인지 함께 받는다. + + 롤러 `Q = 1000 × V × W × E × D × f / N` · 콤펙터 `Q = A × N × H × f × E / P` + """ + if formula == FORMULA_PLATE: + return ( + factors["A"] * factors["N"] * factors["H"] * factors["f"] * factors["E"] / factors["P"] + ) return ( Decimal(1000) * factors["V"] @@ -97,28 +126,45 @@ def compaction_rows(node: dict[str, Any]) -> list[dict[str, Any]]: entry = COMPACTION_ITEMS.get(str(node.get("work_item_code") or "")) if entry is None: return [] + kind = str(entry.get("formula") or FORMULA_ROLLER) for table in node.get("tables") or []: - factors = compaction_factors(table) - if factors is None or factors["N"] == _ZERO: + factors = compaction_factors(table, kind) + if factors is None: continue - capacity = capacity_per_hour(factors) + divisor = factors["P"] if kind == FORMULA_PLATE else factors["N"] + if divisor == _ZERO: + continue + capacity = capacity_per_hour(factors, kind) if capacity <= 0: continue - formula = ( - f"Q = 1000 × {factors['V']:g} × {factors['W']:g} × {factors['E']:g}" - f" × {factors['D']:g} × {factors['f']:g} ÷ {factors['N']:g} = {capacity:g} ㎥/시간" - ) + if kind == FORMULA_PLATE: + formula = ( + f"Q = {factors['A']:g} × {factors['N']:g} × {factors['H']:g}" + f" × {factors['f']:g} × {factors['E']:g} ÷ {factors['P']:g}" + f" = {capacity:g} ㎥/시간" + ) + else: + formula = ( + f"Q = 1000 × {factors['V']:g} × {factors['W']:g} × {factors['E']:g}" + f" × {factors['D']:g} × {factors['f']:g} ÷ {factors['N']:g} = {capacity:g} ㎥/시간" + ) return [ { "machine_code": entry["machine_code"], "capacity_per_hour": capacity, - "cell": "노체다짐 Q(㎥/hr)", + "cell": f"{node.get('name') or '다짐'} Q(㎥/hr)", "ratio_pct": None, "table_id": str(table.get("pum_table_id") or ""), "row_cells": [], - "source_text": ( - f"{formula} · {entry['machine_note']} · {entry['pair_note']}" - f" · {entry['condition_note']}" + "source_text": " · ".join( + part + for part in ( + formula, + entry["machine_note"], + entry.get("pair_note") or "", + entry["condition_note"], + ) + if part ), } ] diff --git a/resources/tester/test_b09_compaction.py b/resources/tester/test_b09_compaction.py index 4c4b7397..4c51fb51 100644 --- a/resources/tester/test_b09_compaction.py +++ b/resources/tester/test_b09_compaction.py @@ -22,6 +22,8 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B09_Estimation.B09_Estimation_MachineProductivity_Compaction import ( # noqa: E402 + COMPACTION_ITEMS, + FORMULA_PLATE, capacity_per_hour, compaction_factors, compaction_rows, @@ -80,3 +82,34 @@ def test_단가가_선다() -> None: assert money.total > 0 # 포설(굴착기)보다 싸야 한다 — 롤러 한 대가 228㎥/hr 로 도는 줄이다. assert money.total < build.book.resolve("B-FP-09-16-01").total + + +def test_콤펙터는_다른_식이다() -> None: + """⚠ 식이 둘이다 — 롤러 `V·W·E·D·f/N` · 콤펙터 `A·N·H·f·E/P`.""" + node = _node("FP-09-14-02") + factors = compaction_factors(node["tables"][0], FORMULA_PLATE) + assert factors == { + "A": Decimal("0.09"), + "N": Decimal(36000), + "H": Decimal("0.15"), + "f": Decimal("1.0"), + "E": Decimal("0.5"), + "P": Decimal(57), + } + + +def test_콤펙터_작업량이_원문_표기와_같다() -> None: + """⚠ 원문이 답까지 적어 두었다 — 「∙ = 4.26 ㎥/시간」. 어긋나면 우리가 잘못 읽은 것이다.""" + node = _node("FP-09-14-02") + factors = compaction_factors(node["tables"][0], FORMULA_PLATE) + assert factors is not None + got = capacity_per_hour(factors, FORMULA_PLATE) + stated = COMPACTION_ITEMS["FP-09-14-02"]["stated_capacity"] + assert abs(got - stated) < Decimal("0.01"), (got, stated) + + +def test_콤펙터_단가도_선다() -> None: + build = cached_build() + title = build.book.titles.get("B-FP-09-14-02") + assert title is not None and title.unit == "㎥" + assert build.book.resolve("B-FP-09-14-02").total > 0