Files
Aislo/resources/tester/test_b09_table_reading.py
T

238 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""표 읽기 (2026-09-13 축 C 1장 · 브레인 차례 ②).
지키는 것
① 밑수가 **절 이름에만** 있는 표는 이름 전체가 「수+단위+당」일 때만 뽑는다(4-2-2 「1,000㎡당」)
② 뭉친 줄: 「굴 삭 기 (무한궤도)」 표기 맞추기 · 비고 줄 거름 · 「-」 칸은 그 갈래에 품 없음
③ 조합 기종 「굴착기+부착용집게」는 공백이 달라도 카탈로그 「부착용 집게」에 붙는다
④ 형식을 안 적은 「굴 삭 기」는 잠정 우선순위로 고르지 않는다(판정 Ⓒ)
⑤ 실무값 대조 — 단목베기 5m 미만이 영월 설계내역 「잡관목제거 벌목(5m미만)」 882원/㎡ 근처로 선다
"""
from __future__ import annotations
from decimal import Decimal
from B08_Quantity.B08_Quantity_Build_WorkItemMaster import basis_from_name
from B09_Estimation.B09_Estimation_ResourceAxis import (
AxisResult,
CatalogEntry,
ResourceCatalog,
match_table,
)
def test_절_이름_밑수는_이름_전체가_밑수일_때만():
assert basis_from_name("1,000㎡당") == (1000.0, "㎡")
assert basis_from_name("100본당") == (100.0, "본")
assert basis_from_name("나무주사(100본당)") == (None, None)
assert basis_from_name("자재의 1회당 표준 운반량") == (None, None)
def _packed(*rows: list[str]) -> dict:
return {
"pum_table_id": "T-시험",
"pum_form": "requirement",
"basis_quantity": 10.0,
"basis_unit": "㎡",
"raw_row": [list(row) for row in rows],
}
MACHINES = [
CatalogEntry("0201-0080", "굴착기(무한궤도)", "machine", "0.8"),
CatalogEntry("0211-0080", "굴착기(타이어)", "machine", "0.8"),
CatalogEntry("0201-0020", "굴착기(무한궤도)", "machine", "0.2"),
CatalogEntry("7206-0020", "부착용 집게", "machine", "0.2"),
CatalogEntry("7206-0100", "부착용 집게", "machine", "1.0"),
CatalogEntry("1002", "보통인부", "labor"),
CatalogEntry("1037", "벌목부", "labor"),
CatalogEntry("1003", "특별인부", "labor"),
]
def _codes(result: AxisResult, variant: str) -> dict[str, Decimal]:
return {r.resource_code: r.amount for r in result.rows if r.variant == variant}
def test_뭉친_줄_굴삭기_표기를_맞추고_비고는_거른다():
table = _packed(
["직경 40㎝이상 60㎝미만", "직경 60㎝이상 80㎝미만"],
["보통인부", "", "인", "1.04(1.17)", "1.08(1.22)"],
["굴 삭 기 (무한궤도)", "0.8㎥", "h", "3.84", "3.52"],
["비고", "- 본 품의 집재거리는 100m까지를 기준하며 매 100m 증가마다 30%씩 가산한다."],
)
result = AxisResult()
match_table({"work_item_code": "FP-13-06-01"}, table, ResourceCatalog(entries=MACHINES), result)
first = _codes(result, "직경 40㎝이상 60㎝미만")
assert first["0201-0080"] == Decimal("0.384") # 무한궤도 — 이름에 형식이 적혀 있음
assert "0211-0080" not in first
assert "FP-13-06-01" not in result.partial_items # 비고 줄로 막히지 않음
def test_조합_기종은_공백이_달라도_붙는다():
table = _packed(
["5m 미만", "5m이상~8m미만"],
["벌목부 보통인부", "", "인 인", "2.14 0.51", "2.80 0.66"],
["굴착기+부착용집게", "0.2㎥", "hr", "2.71", "3.54"],
)
result = AxisResult()
match_table({"work_item_code": "FP-04-02-02"}, table, ResourceCatalog(entries=MACHINES), result)
assert set(_codes(result, "5m 미만")) >= {"0201-0020", "7206-0020"}
assert "FP-04-02-02" not in result.partial_items
def test_형식_없는_굴삭기와_빈칸_표시():
table = _packed(
["식생매트설치", "복 토"],
["특 별 인 부", "", "인", "0.014", "-"],
["굴 삭 기", "0.6 ㎥", "시간", "-", "0.031"],
)
result = AxisResult()
match_table({"work_item_code": "FP-13-13-02"}, table, ResourceCatalog(entries=MACHINES), result)
# 「-」 — 복토 갈래에는 특별인부가 안 든다(자리만 지킴)
assert _codes(result, "식생매트설치") == {"1003": Decimal("0.0014")}
# 형식이 없는 굴삭기는 무한궤도·타이어 중 고르지 않는다 → 막고 사유를 남긴다
assert "굴 삭 기" in result.partial_items.get("FP-13-13-02", "")
def test_단목베기_5m_미만이_실무값_근처로_선다():
from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices
build = build_unit_prices()
assert "FP-04-02-02" not in build.partial_ratio
# 합판거푸집 — 2026-09-14 사용횟수 갈래로 풀림(`test_b09_formwork_use_count`) · 막힘 사유 없음
assert "FP-12-04" not in build.component_gaps
total = build.book.resolve("B-FP-04-02-02#5m미만").total
# 영월 설계내역 1.9.2 「잡관목제거 벌목(5m미만)」 @882 — 노임 연도 차이로 ±5 % 안이면 같은 읽기
assert Decimal("838") <= total <= Decimal("926"), total
def test_단위_글자_없는_분모형_밑수도_본문에서_읽는다():
"""「(인/100m당)」 — 「단위:」 없이 분모만 적은 22 곳(2026-09-13). 「인」은 밑수가 아니다."""
from B08_Quantity.B08_Quantity_Build_WorkItemMaster import basis_from_source
assert basis_from_source(["(인/100m당)", "", "| 공정별 |"], 3) == (100.0, "m")
assert basis_from_source(["(인/본당)", "| 규격 |"], 2) == (1.0, "본")
assert basis_from_source(["(무한궤도/0.7㎥)", "| 규격 |"], 2) == (None, None) # 「당」 없음
_단끊기 = {
"pum_table_id": "F-단끊기",
"pum_form": "requirement",
"basis_quantity": 100.0,
"basis_unit": "m",
"condition_note": ["공정별", "보통인부(인)", "비고"],
"raw_row": [
["보통토사", "경질ㆍ고사점토 및 자갈섞인 점토", "호박돌 섞인 토사", "", ""],
["절 취", "2.4", "3.3", "5.4", ""],
["수평잡기 및 단정리", "0.34", "0.34", "0.34", ""],
["잡석 및 뿌리 정리", "0.36", "0.36", "0.36", ""],
["절·성토면 고르기", "1.17", "1.17", "1.17", ""],
["합계", "2.03", "2.09", "2.23", ""],
],
}
def test_단끊기는_공정_줄을_더하고_작업반장을_20인당_1인_더한다():
catalog = ResourceCatalog(entries=[*MACHINES, CatalogEntry("1001", "작업반장", "labor")])
result = AxisResult()
match_table({"work_item_code": "FP-05-16-01"}, _단끊기, catalog, result)
assert _codes(result, "보통토사") == {"1002": Decimal("0.0427"), "1001": Decimal("0.002135")}
assert _codes(result, "호박돌 섞인 토사")["1002"] == Decimal("0.0727")
assert "FP-05-16-01" not in result.partial_items
# 적어 두지 않은 공종은 같은 모양이어도 더하지 않는다(뭉기기 13-12-1 은 줄마다 단위가 다름)
other = AxisResult()
match_table({"work_item_code": "FP-13-12-01"}, _단끊기, catalog, other)
assert not other.rows
_STONE = [*MACHINES, CatalogEntry("1033", "석공", "labor"), CatalogEntry("1019", "포장공", "labor")]
def test_인력_찰쌓기_두_단_머리는_hwpx_병합대로_돌과_쌓기를_가른다():
"""앞 세 돌은 골·켜 두 잎씩, 호박돌 및 야면석은 한 잎(행 병합) — 7 잎 × 석공·보통인부."""
table = {
"pum_table_id": "F-찰쌓기",
"pum_form": "requirement",
"basis_quantity": 1.0,
"basis_unit": "㎡",
"condition_note": ["뒷길이 (㎝)", "견 치 돌", "깬 돌", "깬 잡 석", "호박돌 및 야면석"],
"raw_row": [
["골쌓기", "켜쌓기", "골쌓기", "켜쌓기", "골쌓기", "켜쌓기", "", ""],
["석 공 (인)", "보통인부 (인)"] * 7 + [""],
["25", *["-"] * 8, "0.12", "0.12", "0.10", "0.10", "0.08", "0.10"],
["35", "0.40", "0.40", "0.44", "0.44", "0.24", "0.24", "0.22", "0.22"]
+ ["0.20", "0.20", "0.18", "0.18", "0.11", "0.14"],
],
}
result = AxisResult()
match_table({"work_item_code": "FP-13-04-04"}, table, ResourceCatalog(entries=_STONE), result)
assert _codes(result, "견치돌 · 켜쌓기 · 뒷길이 35㎝") == {
"1033": Decimal("0.44"),
"1002": Decimal("0.44"),
}
assert _codes(result, "호박돌 및 야면석 · 뒷길이 35㎝") == {
"1033": Decimal("0.11"),
"1002": Decimal("0.14"),
}
assert not _codes(result, "견치돌 · 골쌓기 · 뒷길이 25㎝") # 「-」 — 그 뒷길이엔 품이 없음
assert "FP-13-04-04" not in result.partial_items
def test_콘크리트_포장_작업조는_두께마다_시공량으로_나눈다():
"""12-6 — 두께 칸이 작업조 칸과 엇갈려 병합(hwpx) · 후진 진입 칸은 「50%까지 감」 범위."""
table = {
"pum_table_id": "F-포장",
"pum_form": "reference",
"condition_note": ["배치인원(인)", "포장두께", "시공량(㎥)"],
"raw_row": [
[
"콘크리트믹서트럭 직접타설인경우",
"콘크리트믹서트럭 후진 진입 또는 경운기 등으로 운반인 경우",
],
["포장공", "3", "20㎝", "100", "좌측 시공량의 50%까지 감하여 적용한다."],
["30㎝", "150", "", "", ""],
["보통인부", "3", "", "", ""],
["40㎝", "200", "", "", ""],
],
}
result = AxisResult()
match_table({"work_item_code": "FP-12-06"}, table, ResourceCatalog(entries=_STONE), result)
assert _codes(result, "20㎝") == {"1019": Decimal("0.03"), "1002": Decimal("0.03")}
assert _codes(result, "40㎝")["1019"] == Decimal("0.015")
assert {r.amount_unit for r in result.rows} == {"㎥"} # 시공량 열 머리의 단위
def test_사람이_가른_표_형태는_까닭을_달고_절_번호가_어긋나면_멈춘다():
"""미판정 표 판정(2026-09-13) — 못 가른 표는 싣지 않고, 표 차례가 밀리면 조용히 안 씀."""
import json
from pathlib import Path
import pytest
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Forms import FORM_JUDGMENTS, judged_form
assert judged_form("F0413", "13-4-4")[0] == "reference"
assert judged_form("F9999", "1-1") is None
with pytest.raises(ValueError):
judged_form("F0413", "13-4-5")
assert all(why for _, _, why in FORM_JUDGMENTS.values())
# 마스터에 실렸고, 파형강관 12-11-3 처럼 못 가른 표는 미판정으로 남는다
path = Path("resources/data_work_item_master/work_item_master_2026-01-01.json")
tables = {
t["pum_table_id"]: t
for n in json.loads(path.read_text(encoding="utf-8"))["work_items"]
for t in n["tables"]
}
assert tables["F0413"]["pum_form"] == "reference"
assert tables["F0189"]["pum_form"] == "productivity"
assert tables["F0349"]["pum_form"] == "undetermined"
def test_인력_찰쌓기는_표준경사_표에_막히지_않는다():
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
build = cached_build()
assert "FP-13-04-04" not in build.partial_ratio
assert build.book.resolve("B-FP-13-04-04#깬돌·골쌓기·뒷길이45㎝").labor > 0