feat(tester): 건설 PDF ↔ md 전수 커버리지 검사 + 겸용 연료 둘 갈라 읽기
브레인 B08·B14.
- **PDF ↔ md 커버리지**(`test_const_pdf_coverage.py`) — PDF 글자층을 좌표로 줄로 다시 묶어
값 수를 뽑고 md 전문과 양방향으로 맞댐. 지금 **결손 749**(PDF 에 있는데 md 에 없음) ·
**허구 547**(md 에만 — 대부분 붙은 줄 잔재 `0008900.90` 꼴)
· 쪽별 셈을 목록에 두어 **늘면 빨강**(새 구멍) · `--report` 로 구멍 큰 쪽 스물을 뽑음
· ⚠ 줄 짝은 안 맞춤(PDF 는 칸마다 줄바꿈) · 값처럼 생긴 수만(소수점·세 자리 이상, 연도 뺌) ·
차례 줄·쪽번호 줄은 뺌
- **겸용 연료 둘**(`_build_mach.py`) — 3450-0642 현장가열 표층재생기 `73.7+휘발유54.5` ·
7992-0001 모르타르 믹서 `1.87㎾ 휘발유1.3`
· 둘째 연료는 `secondary_fuel_*`, 전력은 `electric_power_kw`(연료가 아니라 곁값)
· 연료 줄 274 → 276 · 대조 시험도 같은 꼴을 읽게 넓혀 **기계 못 보는 자리 38 → 27**
· 이름표에 새 열 셋 + 줄 수 갱신
- 전체 시험 2,442 통과
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -254,24 +254,24 @@ def parse_fuel(
|
||||
specs = split_values(row[spec_col], len(codes)) if spec_col is not None and spec_col < len(row) else None # fmt: skip
|
||||
for n, code in enumerate(codes):
|
||||
raw = values["fuel_rate_l_per_hour"][n]
|
||||
# ⚠ 연료가 둘인 칸(`73.7+휘발유54.5`)은 한쪽만 적으면 틀림 — 안 싣고 셈만 남김
|
||||
if _num(raw) is None or "+" in raw:
|
||||
fuels, power = split_fuels(raw)
|
||||
if not fuels:
|
||||
# ⚠ 주연료가 수가 아닌 기종(전기 `㎾` · `-`)은 **연료 목록에 안 실음** — ℓ/hr 자료임.
|
||||
skipped.append({"machine_code": code, "what": raw.strip()[:20]})
|
||||
continue
|
||||
record = {
|
||||
"machine_code": code,
|
||||
"machine_name": names.get(code[:4], ""),
|
||||
# 표시 없는 것은 경유([주]① ㉰) · `휘발유0.7` · `중유487.2`
|
||||
"fuel_type": (
|
||||
"gasoline"
|
||||
if re.search(r"휘발유|가솔린", raw)
|
||||
else "heavy_oil"
|
||||
if "중유" in raw
|
||||
else "diesel"
|
||||
),
|
||||
"fuel_rate_l_per_hour": _num(raw),
|
||||
"fuel_type": fuels[0][0],
|
||||
"fuel_rate_l_per_hour": fuels[0][1],
|
||||
}
|
||||
# ⚠ **겸용 연료는 둘 다 싣는다** — 한쪽만 적으면 그만큼 경비가 빈다
|
||||
# (`73.7+휘발유54.5` 현장가열표층재생기 · `1.87㎾ 휘발유1.3` 모르타르 믹서).
|
||||
if len(fuels) > 1:
|
||||
record["secondary_fuel_type"] = fuels[1][0]
|
||||
record["secondary_fuel_rate_l_per_hour"] = fuels[1][1]
|
||||
if power is not None:
|
||||
record["electric_power_kw"] = power
|
||||
for field in ("misc_material_percent_of_fuel", "operator_person_per_day"):
|
||||
value = _num(values[field][n]) if field in values else None
|
||||
if value is not None:
|
||||
@@ -282,6 +282,36 @@ def parse_fuel(
|
||||
return records, bad, source_tables, skipped
|
||||
|
||||
|
||||
#: 겸용 연료 칸을 토막낸다 — `73.7+휘발유54.5` · `1.87㎾ 휘발유1.3` · `중유487.2`.
|
||||
#: ⚠ 표시가 없으면 경유다([주]① ㉰).
|
||||
_FUEL_PIECE_RE = re.compile(r"(휘발유|가솔린|중유|경유)?\s*([\d,]+(?:\.\d+)?)\s*(㎾|kW|kw)?")
|
||||
|
||||
|
||||
def split_fuels(raw: str) -> tuple[list[tuple[str, float]], float | None]:
|
||||
"""연료 칸 → (`[(연료갈래, ℓ/hr), …]`, 전력 ㎾).
|
||||
|
||||
한 칸에 연료가 둘인 기종이 있다 — 그 둘을 **다 싣는다**. 수가 없으면 빈 목록(전기·`-`).
|
||||
"""
|
||||
fuels: list[tuple[str, float]] = []
|
||||
power: float | None = None
|
||||
for mark, number, kw in _FUEL_PIECE_RE.findall(str(raw or "")):
|
||||
value = _num(number)
|
||||
if value is None:
|
||||
continue
|
||||
if kw: # 전력은 연료(ℓ/hr)가 아니라 곁값으로 적어 둔다
|
||||
power = value
|
||||
continue
|
||||
kind = (
|
||||
"gasoline"
|
||||
if mark in ("휘발유", "가솔린")
|
||||
else "heavy_oil"
|
||||
if mark == "중유"
|
||||
else "diesel"
|
||||
)
|
||||
fuels.append((kind, value))
|
||||
return fuels, power
|
||||
|
||||
|
||||
def extract(old_doc: dict, data: bytes) -> tuple[dict, dict]:
|
||||
"""옛 벌 + 8장 md → (새 벌, 알림). 사람 판단 칸은 옛 벌에서 이어받음."""
|
||||
lines = data.decode("utf-8").splitlines()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "mach_base",
|
||||
"effective_date": "2026-01-01",
|
||||
"generated_at": "2026-09-18T13:19:25+09:00",
|
||||
"generated_at": "2026-09-18T14:10:46+09:00",
|
||||
"sources": [
|
||||
{
|
||||
"path": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제8장_건설기계.md",
|
||||
@@ -14132,6 +14132,17 @@
|
||||
"operator_person_per_day": 1,
|
||||
"specification": "400"
|
||||
},
|
||||
{
|
||||
"machine_code": "3450-0642",
|
||||
"machine_name": "현장가열 표층재생기",
|
||||
"fuel_type": "diesel",
|
||||
"fuel_rate_l_per_hour": 73.7,
|
||||
"secondary_fuel_type": "gasoline",
|
||||
"secondary_fuel_rate_l_per_hour": 54.5,
|
||||
"misc_material_percent_of_fuel": 20,
|
||||
"operator_person_per_day": 7,
|
||||
"specification": "479"
|
||||
},
|
||||
{
|
||||
"machine_code": "3530-0015",
|
||||
"machine_name": "스테이빌라이저(안정기)",
|
||||
@@ -15085,6 +15096,15 @@
|
||||
"operator_person_per_day": 1,
|
||||
"specification": "11.19"
|
||||
},
|
||||
{
|
||||
"machine_code": "7992-0001",
|
||||
"machine_name": "모르타르 믹서",
|
||||
"fuel_type": "gasoline",
|
||||
"fuel_rate_l_per_hour": 1.3,
|
||||
"electric_power_kw": 1.87,
|
||||
"misc_material_percent_of_fuel": 2,
|
||||
"specification": "0.3㎥"
|
||||
},
|
||||
{
|
||||
"machine_code": "7993-0020",
|
||||
"machine_name": "양수기",
|
||||
@@ -19124,6 +19144,6 @@
|
||||
"unparsed_price_rows": 0,
|
||||
"unparsed_loss_rows": 10,
|
||||
"unparsed_fuel_rows": 0,
|
||||
"fuel_not_litre_rows": 30
|
||||
"fuel_not_litre_rows": 28
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user