- 새 파일 — Words(식 풀기 · 장비 갈래 · 조건 낱말) · Finger(블록 글 · STmate 자원 → 지문 · 기계 줄 · 직접 줄 호표) · Shares(절 몫 벡터) · Ours(마스터 판 열쇠 · 우리 지문 캐시 tmp/m02_hopyo · 엔진 셈 캐시 · 자재 값 찾기 건너뜀) · Option(옵션 공간 · 거꾸로 찾기 · 견줌) · Fit(손잡이 한 곳 · 순위 · 뒤 셈 · 맞춤셈 캐시) · Combine(조합 · 섞인 줄 몫 먼저) · Verdict(블록 셈 · 맞음 / 다른 점 / 후보) - Match — b_order(이름 · 근거 차례 · 같음 근거 검사 · 옛 판 글 · 조항 오타) · clause(윗마디 0.9 · 나 창 Basis.clause 있으면 그것) · 이름 후보 로직은 접지 않음 - 정답 표 fixtures/m02_hopyo_golden.json(사람 136 · 새 파일 51) · 골든 도우미 - 북부청 92 줄 실측 — 1순위 87 · 다섯 안 91 · 맞음 41 틀림 0 · 다른 점 28 틀림 0 · 조합 맞음 57/66 · 우리 지문 셈 37 초 · 캐시 읽기 0.05 초 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
310 lines
13 KiB
Python
310 lines
13 KiB
Python
"""M02 일위대가 실무자료 엑셀 지문(PLAN 338-1 · 설계 2-2) — 호표 줄 + 이어진 단가산출 블록 →
|
||
직종 품 · 장비 시간 · 자재 수량 · 율 · 계수 · 조건 · 조항 · 몫. 금액 안 씀(식 맨 앞 단가는 뗌).
|
||
|
||
엑셀꼴 = 블록 `글`(머리 뺀 몸 줄)을 줄마다 풂 — 절 `^\\d+\\.` · 작은 절 `^[가-하]\\.` ·
|
||
직종 줄 「직종 : 단가 × 식 = 금액」(우리 인력 이름 · 옛이름) · 장비 = 「노무비 · 경비 · 재료비 :
|
||
단가 ÷ Q」
|
||
+ 지금 장비(노무비 > 경비 > 재료비) · 자재 = 「이름 단가 × 수량 = 금액」 · 「: 별도계상」 ·
|
||
못 풀면 금액 ÷ 단가(「역산」 · 품 되찾기만).
|
||
STmate = 블록 `자원`(L · X · M · S) 품이 곧 품(노무비 · 재료비 · 경비 세 줄 같은 품은 한 번) ·
|
||
섞음 = 이름 · 규격 몫 둘(작은 쪽 인력 · 큰 쪽 장비).
|
||
몫(조합용 · 설계 2-7) = 맨 윗 절마다 노임 · 장비 벡터(`M02_Hopyo_Shares`).
|
||
블록 없는 기계(hr) 줄 = `machine` · 노임 · 장비를 바로 적은 호표 = `direct`.
|
||
값 · 규칙은 336 실험(C4 `s1_excel_fp` · K5 `stmate`) 그대로.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from collections import Counter
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
|
||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||
from M02_MasterTemplete.M02_Hopyo_Shares import trade
|
||
from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, parse_rhs, specnums
|
||
|
||
COST = {"노무비", "재료비", "경비"}
|
||
SKIPW = {
|
||
"소계",
|
||
"계",
|
||
"합계",
|
||
"인부소계",
|
||
"자재소계",
|
||
"주자재소계",
|
||
"운반",
|
||
"상차",
|
||
"기계",
|
||
"다짐",
|
||
"적사",
|
||
"살수",
|
||
"집토",
|
||
"암절취",
|
||
"채집",
|
||
"인력",
|
||
"장비",
|
||
"작업조건",
|
||
"1일시공량",
|
||
"작업면적",
|
||
"1시간작업량",
|
||
"기계채집",
|
||
"구조물터파기",
|
||
}
|
||
_COLON = re.compile(
|
||
r"^\s*(?:\d+\)|[0-9가-힣]\.)?\s*(?P<name>[^::=]{1,40}?)\s*[::]\s*(?P<rhs>.*)$"
|
||
)
|
||
_MAT = re.compile(
|
||
r"^\s*(?P<name>[^\d\s::=][^::=]*?[A-Za-z가-힣)\]])\s+(?P<rhs>[\d,]+(?:\.\d+)?\s*[×xX*÷/].*=.*)$"
|
||
)
|
||
_PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%")
|
||
_COEF = re.compile(
|
||
r"(?<![A-Za-z가-힣])(q|qs|q1|K|k|f|f1|f2|E|Es|Cm|L|V1|V2|T|rt|n|t1|t2|t3|t4)\s*=\s*"
|
||
r"(\d+(?:\.\d+)?)(?![\d.]*\s*[×x*÷/])"
|
||
)
|
||
_HEAD = re.compile(r"^\s*(\d+)\.\s*(.*)$")
|
||
_SUB = re.compile(r"^\s*([가-하])\.\s*(.*)$")
|
||
OLD = re.compile(
|
||
r"((?<![\d,.])20(0\d|1\d|20)(?![\d,.])|(?<!\d)(1\d|20)년|임도품셈|임도표준품셈|사방품셈|사방사업표준품셈)"
|
||
)
|
||
_LASTNUM = re.compile(r"=\s*([\d,]+(?:\.\d+)?)\s*$")
|
||
_HOURS = re.compile(r"(hr|시간)", re.I)
|
||
TRADE_UNITS = ("인",)
|
||
|
||
|
||
def _flat(s: Any) -> str:
|
||
return re.sub(r"\s+", "", str(s or ""))
|
||
|
||
|
||
def _ratio(rhs: str) -> float | None:
|
||
"""식을 못 풀 때만 — 금액 ÷ 단가(품 되찾기용)."""
|
||
m = re.match(r"^\s*([\d,]+(?:\.\d+)?)", rhs)
|
||
n = _LASTNUM.search(rhs)
|
||
if not m or not n:
|
||
return None
|
||
p, a = float(m.group(1).replace(",", "")), float(n.group(1).replace(",", ""))
|
||
return a / p if p else None
|
||
|
||
|
||
def rates_in(text: Any) -> set[float]:
|
||
"""글 속 섞음 비율 후보 — (N%) · 8:2 · 9:1."""
|
||
t = str(text or "")
|
||
out = {round(float(n) / 100, 4) for n in re.findall(r"(\d+(?:\.\d+)?)\s*%", t)}
|
||
out = {v for v in out if 0 < v < 1}
|
||
for a, b in re.findall(r"(\d+)\s*:\s*(\d+)", t):
|
||
a, b = float(a), float(b)
|
||
if a and b and a + b <= 20:
|
||
out |= {round(a / (a + b), 4), round(b / (a + b), 4)}
|
||
return out
|
||
|
||
|
||
def empty(line: dict[str, Any], block: dict[str, Any] | None = None) -> dict[str, Any]:
|
||
block = block or {}
|
||
basis = line.get("근거") or {}
|
||
name, spec = str(line.get("이름") or ""), str(line.get("규격") or "")
|
||
return {
|
||
"이름": name,
|
||
"규격": spec,
|
||
"단위": str(line.get("단위") or ""),
|
||
"출처": [list(x) for x in block.get("출처") or basis.get("출처") or []],
|
||
"인력": {}, # 직종: {품, 밑품, 율, 어떻게}
|
||
"장비": [], # {갈래, 규격, 시간, 밑시간, 율, 절, 글}
|
||
"자재": [], # {이름, 품, 밑품}
|
||
"율": [], # [바탕, %]
|
||
"계수": {},
|
||
"조건": {},
|
||
"이름조건": {k: sorted(v) for k, v in conds(f"{name} {spec}").items()},
|
||
"옛판": False,
|
||
"몫": [],
|
||
}
|
||
|
||
|
||
def _labor(fp: dict, name: str, q: float, base: float, rates: list, how: str) -> None:
|
||
one = fp["인력"].setdefault(name, {"품": 0.0, "밑품": 0.0, "율": [], "어떻게": []})
|
||
one["품"] += q
|
||
one["밑품"] += base
|
||
one["율"] += rates
|
||
one["어떻게"].append(how)
|
||
|
||
|
||
def _round(fp: dict[str, Any]) -> dict[str, Any]:
|
||
for one in fp["인력"].values():
|
||
one["품"], one["밑품"] = round(one["품"], 6), round(one["밑품"], 6)
|
||
return fp
|
||
|
||
|
||
def excel(block: dict[str, Any] | None, line: dict[str, Any]) -> dict[str, Any]:
|
||
"""HP 블록(설계 3-1) + 그 줄(이름 · 규격 · 단위) → 지문 · 블록이 없거나 글 · 자원이 없으면
|
||
기계(hr) 줄 지문(장비 1 시간) 또는 빈 지문."""
|
||
if block and block.get("자원"):
|
||
return _round(_stmate(block, line))
|
||
if block and block.get("글"):
|
||
return _round(_text(block, line))
|
||
return machine(line)
|
||
|
||
|
||
def machine(line: dict[str, Any]) -> dict[str, Any]:
|
||
"""블록 없는 기계(hr) 줄 → 장비 [{갈래, 규격, 시간 1}]."""
|
||
fp = empty(line)
|
||
text = f"{fp['이름']} {fp['규격']}"
|
||
types = mtypes(text)
|
||
if types:
|
||
hours = 1.0 if _HOURS.search(fp["단위"]) else None
|
||
one = {"갈래": sorted(types), "규격": sorted(specnums(text)), "시간": hours}
|
||
fp["장비"] = [{**one, "밑시간": None, "율": [], "절": "0", "글": fp["이름"]}]
|
||
fp["조건"] = fp["이름조건"]
|
||
return fp
|
||
|
||
|
||
def _text(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]:
|
||
"""엑셀꼴 — 블록 글 줄마다 풂(C4 `north`)."""
|
||
fp = empty(line, block)
|
||
head = f"{block.get('번', '')}.{block.get('이름', '')} [{block.get('단위', '')}]"
|
||
text = [head, *[str(t) for t in block.get("글") or []]]
|
||
front = [t for t in text[:5]] + [t for t in text if "※" in t or "작업조건" in t]
|
||
head_text = " ".join([fp["이름"], fp["규격"], *front])
|
||
fp["조건"] = {k: sorted(v) for k, v in conds(head_text).items()}
|
||
fp["옛판"] = bool(OLD.search(" ".join(text[:6])))
|
||
sec, sub, mach, label = 0, "", None, ""
|
||
got_cost: dict[tuple, tuple] = {}
|
||
item_rates = rates_in(f"{fp['이름']} {fp['규격']}")
|
||
sec_rates: set[float] = set()
|
||
|
||
def unmix(got: dict | None) -> dict | None:
|
||
"""율 메모가 없는데 맨 인자가 섞음 비율 후보와 같으면 율로 가름."""
|
||
if got and not got["율"]:
|
||
for v in got.get("인자") or []:
|
||
if round(v, 4) in item_rates | sec_rates and 0 < v < 1:
|
||
return {**got, "밑품": got["밑품"] / v, "율": [[round(v, 4), "섞음"]]}
|
||
return got
|
||
|
||
for raw in text[1:]:
|
||
row = raw.rstrip()
|
||
h, s = _HEAD.match(row), _SUB.match(row)
|
||
if h or s:
|
||
if h:
|
||
sec, sub = int(h.group(1)), ""
|
||
sec_rates = rates_in(h.group(2))
|
||
else:
|
||
sub = s.group(1)
|
||
sec_rates = sec_rates | rates_in(s.group(2))
|
||
title = (h or s).group(2)
|
||
ts = mtypes(title)
|
||
mach = (ts, specnums(title)) if ts else None
|
||
label = title.strip() if ts else ""
|
||
for k, v in _PCT.findall(row):
|
||
fp["율"].append([k, float(v)])
|
||
cm = _COLON.match(row)
|
||
name = re.sub(r"^[0-9가-힣]\.", "", _flat(cm.group("name"))) if cm else ""
|
||
who = hopyo_basis.trade_names().get(name) if cm else None
|
||
if who:
|
||
rhs = cm.group("rhs")
|
||
got = unmix(parse_rhs(rhs))
|
||
if got:
|
||
_labor(fp, who, got["품"], got["밑품"], got["율"], "식")
|
||
elif r := _ratio(rhs):
|
||
_labor(fp, who, r, r, [], "역산")
|
||
continue
|
||
if cm and name in COST:
|
||
got = unmix(parse_rhs(cm.group("rhs")))
|
||
if got and mach:
|
||
key = (sec, sub, label)
|
||
prio = 0 if (name == "노무비" and got["단가"] > 0) else 1 if name == "경비" else 2
|
||
if key not in got_cost or prio < got_cost[key][0]:
|
||
got_cost[key] = (prio, mach, got)
|
||
continue
|
||
if cm and name not in SKIPW and re.search(r"(별도|별산)", cm.group("rhs")):
|
||
fp["자재"].append({"이름": cm.group("name").strip(), "품": None, "밑품": None})
|
||
continue
|
||
mm = _MAT.match(row)
|
||
if mm and not cm and (got := parse_rhs(mm.group("rhs"))):
|
||
fp["자재"].append(
|
||
{"이름": mm.group("name").strip(), "품": got["품"], "밑품": got["밑품"]}
|
||
)
|
||
continue
|
||
if not (h or s):
|
||
ts = mtypes(row)
|
||
if ts and "%" not in row and not cm:
|
||
mach, label = (ts, specnums(row)), row.strip()
|
||
for k, v in _COEF.findall(row):
|
||
fp["계수"].setdefault(k, float(v))
|
||
for (sec_, sub_, lab), (_, (ts, sp), got) in got_cost.items():
|
||
where = f"{sec_}.{sub_}" if sub_ else str(sec_)
|
||
one = {"갈래": sorted(ts), "규격": sorted(sp), "시간": got["품"], "밑시간": got["밑품"]}
|
||
fp["장비"].append({**one, "율": got["율"], "절": where, "글": lab})
|
||
fp["몫"] = shares.sections(text[1:])
|
||
return fp
|
||
|
||
|
||
def _stmate(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]:
|
||
"""STmate — 자원 품이 곧 품(K5 `stmate`)."""
|
||
fp = empty(line, block)
|
||
text = [str(t) for t in block.get("글") or []][:60]
|
||
fp["조건"] = {
|
||
k: sorted(v) for k, v in conds(" ".join([fp["이름"], fp["규격"], *text[:4]])).items()
|
||
}
|
||
fp["옛판"] = bool(OLD.search(" ".join(text[:4])))
|
||
for t in text:
|
||
for k, v in _COEF.findall(t):
|
||
fp["계수"].setdefault(k, float(v))
|
||
for k, v in _PCT.findall(t):
|
||
fp["율"].append([k, float(v)])
|
||
rates = rates_in(f"{fp['이름']} {fp['규격']}")
|
||
lab_rate = min(rates) if len(rates) >= 2 else None
|
||
mach_rate = max(rates) if len(rates) >= 2 else None
|
||
seen: Counter = Counter()
|
||
for x in block.get("자원") or []:
|
||
kind, q = x.get("갈래"), x.get("품")
|
||
nm, sp = str(x.get("이름") or ""), str(x.get("규격") or "")
|
||
if not q:
|
||
continue
|
||
seen[(x.get("코드"), round(float(q), 9))] += 1
|
||
if seen[(x.get("코드"), round(float(q), 9))] > 1: # 노무비 · 재료비 · 경비 세 줄 같은 품
|
||
continue
|
||
q = float(q)
|
||
ts = mtypes(f"{nm} {sp}")
|
||
if kind == "L":
|
||
if who := trade(nm):
|
||
base = q / lab_rate if lab_rate else q
|
||
_labor(fp, who, q, base, [[lab_rate, "섞음"]] if lab_rate else [], "QTY")
|
||
elif kind == "X" or (kind == "S" and ts):
|
||
one = {"갈래": sorted(ts), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q}
|
||
base = q / mach_rate if mach_rate else q
|
||
fp["장비"].append({**one, "밑시간": base, "율": [], "절": "0", "글": f"{nm} {sp}"})
|
||
elif kind in ("M", "S"):
|
||
fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": q, "밑품": q})
|
||
fp["몫"] = shares.sections_stm(block.get("자원") or [], text)
|
||
return fp
|
||
|
||
|
||
def direct(hopyo: dict[str, Any]) -> dict[str, Any] | None:
|
||
"""노임 · 장비를 바로 적은 호표 → 줄 수량 합 지문(호표 1 단위) · 근거글 출처 — 없으면 None."""
|
||
lines = hopyo.get("줄") or []
|
||
fp = empty({k: hopyo.get(k) for k in ("이름", "규격", "단위")})
|
||
texts = [str(t) for t in hopyo.get("근거글") or []]
|
||
texts += [f"{x.get('이름')} {x.get('규격')}" for x in lines if x.get("갈래") == "근거글"]
|
||
fp["출처"] = hopyo_basis.sources(" ".join(texts))
|
||
hit = False
|
||
for x in lines:
|
||
q = x.get("수량")
|
||
if not isinstance(q, (int, float)) or q <= 0:
|
||
continue
|
||
if kind_of(x) == "노임" and (who := trade(x.get("이름"))):
|
||
_labor(fp, who, float(q), float(q), [], "QTY")
|
||
hit = True
|
||
elif kind_of(x) == "기계":
|
||
text = f"{x.get('이름')} {x.get('규격')}"
|
||
if ts := mtypes(text):
|
||
one = {"갈래": sorted(ts), "규격": sorted(specnums(text)), "시간": float(q)}
|
||
fp["장비"].append({**one, "밑시간": float(q), "율": [], "절": "0", "글": text})
|
||
hit = True
|
||
return _round(fp) if hit and fp["인력"] else None
|
||
|
||
|
||
def kind_of(line: dict[str, Any]) -> str:
|
||
"""줄 갈래 + 「노임」(단위 인 + 우리 직종 · 나 창 읽기 전에도)."""
|
||
kind = str(line.get("갈래") or "")
|
||
unit = _flat(line.get("단위"))
|
||
if kind in ("기타", "자재", "노임") and unit in TRADE_UNITS and trade(line.get("이름")):
|
||
return "노임"
|
||
return kind
|