feat(B09): 플레이트 콤펙터 다짐(9-14-2)도 세움 — 다짐 식 둘째

노체다짐과 식이 다름 — 롤러는 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 23:29:30 +09:00
co-authored by Claude Opus 5
parent 6b0ae4f0fd
commit 6cf7de9e82
2 changed files with 102 additions and 23 deletions
@@ -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) _ZERO = Decimal(0)
#: 계수 이름 — 표 첫 칸이 「V(다짐속도,km/hr)」처럼 기호+설명이라 **앞 글자**로 가른다. #: 계수 이름 — 표 첫 칸이 「V(다짐속도,km/hr)」처럼 기호+설명이거나 「A」처럼 기호뿐이다.
_KEYS = ("V", "W", "E", "D", "f", "N") #: ⚠ **식이 둘이다** — 롤러는 `V·W·E·D·f/N`, 플레이트 콤펙터는 `A·N·H·f·E/P` 다.
_RE_HEAD = re.compile(r"^([VWEDfN])\s*[(]") FORMULA_ROLLER = "roller"
_RE_NUMBER = re.compile(r"^-?\d+(?:\.\d+)?$") 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]] = { COMPACTION_ITEMS: dict[str, dict[str, Any]] = {
"FP-09-16-02": { "FP-09-16-02": {
"formula": FORMULA_ROLLER,
"machine_code": "1306-0100", # 진동롤러(자주식) 10ton "machine_code": "1306-0100", # 진동롤러(자주식) 10ton
"machine_note": "산림품셈 9-16-2 [주]① 「장비는 자주식 진동롤러(10ton)를 적용한다」", "machine_note": "산림품셈 9-16-2 [주]① 「장비는 자주식 진동롤러(10ton)를 적용한다」",
"condition_note": ( "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: def _number(cells: list[str]) -> Decimal | None:
for cell in cells: for cell in cells:
text = str(cell or "").strip() text = str(cell or "").strip()
if _RE_NUMBER.match(text): found = _RE_NUMBER.match(text)
return Decimal(text) if found:
return Decimal(found.group(0).replace(",", ""))
return None 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` — **채워 넣지 않는다.**""" """표에서 여섯 계수를 뽑는다. 하나라도 없으면 `None` — **채워 넣지 않는다.**"""
found: dict[str, Decimal] = {} found: dict[str, Decimal] = {}
for row in table.get("raw_row") or []: 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:]) value = _number(cells[1:])
if value is not None: if value is not None:
found.setdefault(head.group(1), value) found.setdefault(head.group(1), value)
if set(found) != set(_KEYS): keys = set(_KEYS[formula])
if not keys <= set(found):
return None return None
return found return {key: found[key] for key in keys}
def capacity_per_hour(factors: dict[str, Decimal]) -> Decimal: def capacity_per_hour(factors: dict[str, Decimal], formula: str = FORMULA_ROLLER) -> Decimal:
"""Q = 1000 × V × W × E × D × f / N (㎥/시간).""" """시간당 작업량(㎥/시간). **식이 둘**이라 어느 식인지 함께 받는다.
롤러 `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 ( return (
Decimal(1000) Decimal(1000)
* factors["V"] * 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 "")) entry = COMPACTION_ITEMS.get(str(node.get("work_item_code") or ""))
if entry is None: if entry is None:
return [] return []
kind = str(entry.get("formula") or FORMULA_ROLLER)
for table in node.get("tables") or []: for table in node.get("tables") or []:
factors = compaction_factors(table) factors = compaction_factors(table, kind)
if factors is None or factors["N"] == _ZERO: if factors is None:
continue 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: if capacity <= 0:
continue continue
formula = ( if kind == FORMULA_PLATE:
f"Q = 1000 × {factors['V']:g} × {factors['W']:g} × {factors['E']:g}" formula = (
f" × {factors['D']:g} × {factors['f']:g} ÷ {factors['N']:g} = {capacity:g} ㎥/시간" 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 [ return [
{ {
"machine_code": entry["machine_code"], "machine_code": entry["machine_code"],
"capacity_per_hour": capacity, "capacity_per_hour": capacity,
"cell": "노체다짐 Q(㎥/hr)", "cell": f"{node.get('name') or '다짐'} Q(㎥/hr)",
"ratio_pct": None, "ratio_pct": None,
"table_id": str(table.get("pum_table_id") or ""), "table_id": str(table.get("pum_table_id") or ""),
"row_cells": [], "row_cells": [],
"source_text": ( "source_text": " · ".join(
f"{formula} · {entry['machine_note']} · {entry['pair_note']}" part
f" · {entry['condition_note']}" for part in (
formula,
entry["machine_note"],
entry.get("pair_note") or "",
entry["condition_note"],
)
if part
), ),
} }
] ]
+33
View File
@@ -22,6 +22,8 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_MachineProductivity_Compaction import ( # noqa: E402 from B09_Estimation.B09_Estimation_MachineProductivity_Compaction import ( # noqa: E402
COMPACTION_ITEMS,
FORMULA_PLATE,
capacity_per_hour, capacity_per_hour,
compaction_factors, compaction_factors,
compaction_rows, compaction_rows,
@@ -80,3 +82,34 @@ def test_단가가_선다() -> None:
assert money.total > 0 assert money.total > 0
# 포설(굴착기)보다 싸야 한다 — 롤러 한 대가 228㎥/hr 로 도는 줄이다. # 포설(굴착기)보다 싸야 한다 — 롤러 한 대가 228㎥/hr 로 도는 줄이다.
assert money.total < build.book.resolve("B-FP-09-16-01").total 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