Files
Aislo/M02_MasterTemplete/M02_Hopyo_Finger.py
T
eomsangdonandClaude Opus 5.5 022d6375e1 refactor(776-3): 지문 형식 모름 — 단위 줄 투영 한 벌 · 몫 한 벌 · 블록 단가 = 항목 · clause 대체 몸 · 오타 정규식 걷음 · 맞춤 캐시 = HP 지문 칸
- Finger — 풀이 두 벌(_text · _stmate) 걷고 블록 단위 줄 투영 하나(_units) · 글은 조건 · 옛 판 · 계수 · 「…의 N %」 만 · 셈 밖 줄 안 씀
- Shares — 몫 = 단위 줄을 절(제목 · 윗절)로 묶음 한 벌 · 식 다시 풀기(qty_of · sections_stm) 걷음 · trade = ReadEst.trade
- Elem.prices = 단위 줄 단가(글 다시 안 풂 · machines 갈래 안 건드림)
- Match.clause = Basis.clause 만 · 대체 몸 · _TYPO_CLAUSE 걷음(오타는 읽기 ReadEst.sources) · fix_typo = 사본(Line 이 부름)
- Fit.cache_path — HP 지문 칸(올린 엑셀 sha256 + 판) · 파일 그대로면 다시 안 읽음(lru) · 붙여넣기 HP 만 파일 해시
- 지문 전후 = 셈 단위 275 중 150 같음 · 다름 = 장비 절 · 글(쓰는 곳 없음) 100 · 고침(STmate 기계 두 묶음 · 거창 자재 · 끝 점 직종 · 옛 몫 금액 ÷ 단가 엉터리 · 제외금액 줄) — 골든 1위 줄마다 · 불리는 블록 206 갈래 · 1위 · 상태 전후 같음(162/177 · 기초 80 · SK 37 · 대기 89)
- S1 다시 굳힘 = 봉화 · 울진대흥 · 울진소광 자원(같은 코드 두 묶음 따로) · S2 다시 굳힘 125/275(위 까닭)
- 시험 고침 — finger(블록 = ReadEst.items · STmate 단위 줄 · 셈 밖) · fit 옛 HP(항목 · 글 없음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:20:17 +09:00

186 lines
8.7 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.
"""M02 일위대가 실무자료 엑셀 지문(PLAN 338-1 · 설계 2-2 · 776-3) — 호표 줄 + 이어진 단가산출 블록 →
직종 품 · 장비 시간 · 자재 수량 · 율 · 계수 · 조건 · 조항 · 몫. 금액 안 씀.
형식 모름 — 블록 단위 줄(`항목` · `M02_Hopyo_UnitLine`)의 투영:
L 노무 = 직종 품 · 식이 있으면 맨 인자가 섞음 비율 후보(호표 이름 · 규격 · 절 제목의 (N%) ·
8:2)와 같을 때 율로 가름 · 식 없는 줄(QTY)은 호표 이름 비율 둘 이상이면 작은 쪽(인력)
X 기계 = 장비 {갈래, 규격, 시간, 밑시간, 율, 절, 글} · 식 없는 줄은 큰 비율(장비)로 가름
M 자재 = 식(수량) · 별도계상(품 없음) · E 값 줄에 수량이 있으면 장비 글이면 장비 · 아니면 자재
셈 밖 · 율 · 하위 줄 = 안 씀(율 % 는 글에서).
글(블록 머리 「N.이름 [단위]」 + 몸 글) = 조건 · 옛 판 · 계수(q · K · f …) · 글 속 「…의 N %」.
몫(조합용 · 설계 2-7) = 절마다 노임 · 장비 벡터(`M02_Hopyo_Shares.sections`).
블록 없는 기계(hr) 줄 = `machine` · 노임 · 장비를 바로 적은 호표 = `direct`.
"""
from __future__ import annotations
import re
from typing import Any
from M02_MasterTemplete import M02_Hopyo_ReadEst as hopyo_est
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, specnums
_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*÷/])"
)
_SECTION = re.compile(r"^\s*(\d+\.|[가-하]\.)")
OLD = re.compile(
r"((?<![\d,.])20(0\d|1\d|20)(?![\d,.])|(?<!\d)(1\d|20)년|임도품셈|임도표준품셈|사방품셈|사방사업표준품셈)"
)
_HOURS = re.compile(r"(hr|시간)", re.I)
TRADE_UNITS = ("인",)
rates_in = hopyo_est.rates_in
def _flat(s: Any) -> str:
return re.sub(r"\s+", "", str(s or ""))
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("항목") or block.get("글")):
return _round(_units(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(fp: dict[str, Any], block: dict[str, Any]) -> None:
"""블록 글 → 조건(머리 + 앞 넉 줄 + ※ · 작업조건 줄) · 옛 판 · 계수 · 「…의 N %」."""
head = f"{block.get('번', '')}.{block.get('이름', '')} [{block.get('단위', '')}]"
text = [head, *[str(t) for t in block.get("글") or []]]
front = text[:5] + [t for t in text if "※" in t or "작업조건" in t]
got = conds(" ".join([fp["이름"], fp["규격"], *front]))
fp["조건"] = {k: sorted(v) for k, v in got.items()}
fp["옛판"] = bool(OLD.search(" ".join(text[:6])))
for row in text[1:]:
fp["율"] += [[k, float(v)] for k, v in _PCT.findall(row)]
if not _SECTION.match(row):
for k, v in _COEF.findall(row):
fp["계수"].setdefault(k, float(v))
def _unmix(one: dict[str, Any], rates: set[float]) -> tuple[float, list]:
"""(밑품, 율) — 율 메모가 없는데 맨 인자가 섞음 비율 후보와 같으면 율로 가름."""
if not one.get("율"):
for v in one.get("인자") or []:
if round(v, 4) in rates and 0 < v < 1:
return one["밑품"] / v, [[round(v, 4), "섞음"]]
return one["밑품"], one.get("율") or []
def _units(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]:
fp = empty(line, block)
_text(fp, block)
item_rates = rates_in(f"{fp['이름']} {fp['규격']}")
pair = sorted(item_rates) if len(item_rates) >= 2 else []
lab_rate, mach_rate = (pair[0], pair[-1]) if pair else (None, None)
for one in block.get("항목") or []:
kind, q, how = one.get("묶음"), one.get("품"), one.get("어떻게")
if one.get("밖") or (q is None and how != "별도"):
continue
text = f"{one.get('이름') or ''} {one.get('규격') or ''}".strip()
rates = item_rates | set(one.get("절율") or [])
if kind == "L" and (who := one.get("직종") or trade(one.get("이름"))):
if how == "QTY":
base, r = (q / lab_rate, [[lab_rate, "섞음"]]) if lab_rate else (q, [])
else:
base, r = _unmix(one, rates) if how == "식" else (one["밑품"], one["율"])
_labor(fp, who, q, base, r, how)
elif kind == "X" or (kind == "E" and mtypes(text)):
if how == "QTY":
base, r = (q / mach_rate if mach_rate else q), []
else:
base, r = _unmix(one, rates)
sizes = specnums(one.get("규격") or "") | specnums(one.get("이름") or "")
got = {"갈래": sorted(mtypes(text)), "규격": sorted(sizes), "시간": q}
fp["장비"].append({**got, "밑시간": base, "율": r, "절": one.get("절"), "글": text})
elif kind in ("M", "E") and (how in ("식", "별도", "QTY")):
fp["자재"].append({"이름": text, "품": q, "밑품": one.get("밑품", q)})
fp["몫"] = shares.sections(block.get("항목") or [])
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_est.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