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