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:
@@ -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
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user