- 잣대 = 같은 파일 · 같은 때의 구성 줄(손료 계수 · 가격 · 주연료 · 조종원) — 우리 기계표 값과 맞대지 않음(요구 773-3) - M02_Machine_Find.peers · narrow — 구성이 같은 단독 기계 이름 · 규격으로 다시 찾아 후보 가운데 키 하나면 이음 · 여럿 · 없음 = 그대로 - EST 원파일 MachMainDat 묶기(M02_San_Estx.makes) · EST 엑셀 「기계경비」 시트(M02_San_Xlsx.makes) → 자원 「같은구성」 - 저장 묶음(M02_Doc_FromNative) · 줄 만들기(M02_Doc_NativeEst · M02_San_Emit) 가 「같은구성」 을 넘김 - 앞서 짠 우리 표 값 대조는 걷음 · STC = 구성 표(BDQTY) 난독화로 없음 · STmate 엑셀 중기사용료 = 같은 파일 단독 짝 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
668 lines
31 KiB
Python
668 lines
31 KiB
Python
"""EST 산근 엑셀 → 810-1 구조 줄(PLAN 812-1 · `M02_San_Estx.cost_line` 과 같은 칸 · 읽기만 · 서버 단독).
|
||
|
||
엑셀 「단가산출」 시트 = 원파일 LineData 를 보인 글(제어문자 표지 없음) + 줄 금액 칸(원파일 줄 TC NC GC JC ·
|
||
0 = 빈칸 · 소계 61 = 비목 칸만) → 표지를 글 · 금액 칸 · 같은 책 단가표(노임 · 자재 · 기계 · 견적 · 단가산출
|
||
총괄표)로 다시 정함(`연결뜻` · 표지 「엑셀 추정」 · 자원 코드 · 자리 = 비움):
|
||
변수 줄 「이름 = 식 = 값」 → 값 = 결과(`\\x12`) · 「이름 = 값」 → 대입(결과 없음) · 금액 줄 첫 식 조각의 수가
|
||
앞 대입 값과 같으면 변수(`\\x11` · 상수 아님) ·
|
||
합계 머리(「합 계」) 아래 노무비 · 재료비 · 경비 · 계 줄 = 합계(`\\x1a` · 비목 L M S T) ·
|
||
「* 단산 N 참조 *」 · 「* 견적 N 참조 *」 · 기계 이름(+규격) = 세 줄 머리(104 · 101 · 105) → 아래
|
||
「노무비 · 재료비 · 경비 : 단가 …」(단가 = 그 자원 비목 값) = 세 줄 · 「소 계」 = 계 줄 ·
|
||
노임 · 자재 이름 + 그 단가 수 = 자원 줄(5 · 6 · 비목 = 금액 칸) ·
|
||
「…소계 : 수」(금액 칸 = 비목만) = 소계 61(줄참조) · 「수 × n% = 값」 + 금액 칸 = 율 58(줄참조) ·
|
||
「(A 수 + 수) × n%」 = 율 62(구간 합) · 「= A수」 = 구간 표지(`\\x06A\\x06`) · 그 밖 = 글 줄.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||
|
||
SHEET = "단가산출"
|
||
LINK = {"자재": 5, "노임": 6, "견적": 101, "산근": 104, "기계": 105, "소계": 61, "율": 58}
|
||
MEAN = {5: "자재", 6: "노임", 101: "견적", 104: "산근", 105: "기계", 61: "소계", 58: "율"}
|
||
MEAN[62] = "율"
|
||
LABEL = {"노무비": "L", "재료비": "M", "경비": "S", "계": "T"}
|
||
NUM = r"\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?|\.\d+" # 「.1」 = 엑셀 꼴 0.1
|
||
_NUM = re.compile(r"(?<![A-Za-z0-9_.,])(-?(?:" + NUM + r"))\.?(?![\d,])")
|
||
_HEAD = re.compile(r"^(\d+)\.(.*?)\[([^\[\]]*)\]\s*$")
|
||
_HEADING = re.compile(r"^\s*(?:\(\d+\)|(?:\d+|[가-하])\s*[.)])\s*")
|
||
_REF = re.compile(r"\*\s*(단산|견적)\s*(\d+)\s*참조\s*\*")
|
||
_ASSIGN = re.compile(r"(?<![A-Za-z0-9_])([A-Za-z][A-Za-z0-9]*)\s*(?:\([^)=]*\))?\s*=(?!=)")
|
||
_GROUP62 = re.compile(r"^\s*\(?\s*([A-Z])\s*(" + NUM + r")((?:\s*\+\s*(?:" + NUM + r"))*)")
|
||
_UNITS = {"hr", "m", "km", "kg", "ton", "sec", "min", "mm", "cm", "ea", "t"}
|
||
_COLON = re.compile(r"[::]")
|
||
|
||
|
||
def _v(s: str) -> float:
|
||
return float(s.replace(",", "").rstrip("."))
|
||
|
||
|
||
def _close(a: float, b: float) -> bool:
|
||
return abs(a - b) < 1e-6 * max(1.0, abs(b))
|
||
|
||
|
||
def _span(text: str, name: str, start: int = 0) -> tuple[int, int] | None:
|
||
"""빈칸을 건너 같은 글자 차례 자리(이름 · 규격) · 없음 None."""
|
||
chars = [re.escape(c) for c in sx.flat(name)]
|
||
if not chars:
|
||
return None
|
||
m = re.compile(r"\s*".join(chars)).search(text, start)
|
||
return (m.start(), m.end()) if m else None
|
||
|
||
|
||
# ── 시트 → 블록 ───────────────────────────────────────────────────────
|
||
def blocks(rows: list[list[Any]]) -> list[dict[str, Any]]:
|
||
"""단가산출 시트 → [{번, 머리, 행, 저장, 줄 [{행, 엑셀행, 보임, 돈}]}](머리 = B 열 「N.이름[단위]」)."""
|
||
cols = sx.money_cols(rows) or [2, 3, 4, 5]
|
||
split = cols[0] > 6 # 거창 꼴 — 글자 칸 나눔(열 = 화면 자리 · B 열 = 머리만)
|
||
out: list[dict[str, Any]] = []
|
||
for i, r in enumerate(rows):
|
||
head = r[1] if len(r) > 1 else None
|
||
m = _HEAD.match(head) if isinstance(head, str) and not head[:1].isspace() else None
|
||
if m and i > 2:
|
||
out.append({"번": int(m.group(1)), "머리": head, "행": i + 1, "줄": []})
|
||
out[-1]["저장"] = sx.money_of(r, cols)
|
||
continue
|
||
if not out:
|
||
continue
|
||
if split:
|
||
cells = [(j, sx._cell(v)) for j, v in enumerate(r[2 : cols[0]], 2)]
|
||
text = sx.layout([(j, v) for j, v in cells if v.strip()], 2)
|
||
else:
|
||
text = sx._cell(head)
|
||
one = {"행": len(out[-1]["줄"]) + 1, "엑셀행": i + 1, "보임": text, "칸": split}
|
||
out[-1]["줄"].append(one | {"돈": sx.money_of(r, cols)})
|
||
for b in out: # 끝 빈 줄(시트 꼬리)
|
||
while b["줄"] and not b["줄"][-1]["보임"].strip() and not b["줄"][-1]["돈"]:
|
||
b["줄"].pop()
|
||
return out
|
||
|
||
|
||
# ── 줄 ────────────────────────────────────────────────────────────────
|
||
def _label(text: str) -> str | None:
|
||
"""「노무비 :」 · 「경 비 :」 · 「계 :」 → 비목 글자 · 아니면 None."""
|
||
m = _COLON.search(text)
|
||
if not m:
|
||
return None
|
||
return LABEL.get(sx.flat(_HEADING.sub("", text[: m.start()])))
|
||
|
||
|
||
def _nums(text: str, lo: int = 0, hi: int | None = None) -> list[tuple[int, int, float]]:
|
||
hi = len(text) if hi is None else hi
|
||
return [(m.start(1), m.end(1), _v(m.group(1))) for m in _NUM.finditer(text, lo, hi)]
|
||
|
||
|
||
def _result(text: str) -> tuple[int, int, float, int | None] | None:
|
||
"""마지막 「=」 뒤 첫 수(구간 글자 「A」 붙음 허용) → (시작, 끝, 값, 글자 자리)."""
|
||
eq = text.rfind("=")
|
||
if eq < 0:
|
||
return None
|
||
m = re.match(r"\s*(?:([A-Z])\s*)?(-?(?:" + NUM + r"))\.?(?![\d,])", text[eq + 1 :])
|
||
if not m:
|
||
return None
|
||
at = eq + 1
|
||
return at + m.start(2), at + m.end(2), _v(m.group(2)), at + m.start(1) if m.group(1) else None
|
||
|
||
|
||
class Lines:
|
||
"""블록 한 벌 줄 → 구조 줄."""
|
||
|
||
def __init__(self, res: dict[str, list[dict]], tags: list[str]) -> None:
|
||
self.res, self.tags = res, tags
|
||
self.vars: dict[str, float] = {} # 이름 → 마지막 대입 값
|
||
self.ctx: dict | None = None # 세 줄 머리 자원 {종류, 자원}
|
||
self.triple = False # 바로 앞이 세 줄
|
||
self.heads: list[dict] = [] # 세 줄 머리 줄들
|
||
self.lines: list[dict[str, Any]] = []
|
||
|
||
# ── 꼴 ─────────────────────────────────────────────────────────
|
||
def blank(self, src: dict) -> dict[str, Any]:
|
||
return {
|
||
"행": src["행"],
|
||
"엑셀행": src["엑셀행"],
|
||
"글": src["보임"],
|
||
"보임": src["보임"],
|
||
"품명": None,
|
||
"규격": None,
|
||
"단위": None,
|
||
"단": 0,
|
||
"색인": 0,
|
||
"연결": 0,
|
||
"연결뜻": None,
|
||
"비목": None,
|
||
"자원": None,
|
||
"연결단가": [],
|
||
"변수": [],
|
||
"줄참조": [],
|
||
"결과": [],
|
||
"합계": False,
|
||
"자리": {"자리수": None, "방법": None},
|
||
"표지": None,
|
||
"기계": None,
|
||
"산근": None,
|
||
"저장": src.get("돈"),
|
||
"추정": False,
|
||
"_marks": [],
|
||
"_칸": src.get("칸"),
|
||
}
|
||
|
||
def resource(self, kind: str, one: dict) -> dict[str, Any]:
|
||
code = (
|
||
f"단산{one['번']}" if kind == "산근" else f"{one['표']}#{one['번']}"
|
||
) # 원 코드 없음 = 표 줄 번
|
||
out = {"코드": code, "종류": kind, "표": one["표"]}
|
||
out |= {
|
||
k: one[k]
|
||
for k in ("이름", "규격", "단위") + (("같은구성",) if "같은구성" in one else ())
|
||
}
|
||
return out | {"저장": dict(one["값"])}
|
||
|
||
def link(self, ln: dict, kind: str, one: dict | None) -> None:
|
||
ln["연결"] = LINK[kind] if kind in LINK else 62
|
||
ln["연결뜻"] = MEAN.get(ln["연결"])
|
||
ln["추정"] = True
|
||
if one is not None:
|
||
ln["자원"] = self.resource(kind, one)
|
||
if kind == "산근":
|
||
ln["산근"] = ln["자원"]["코드"]
|
||
|
||
@staticmethod
|
||
def money_part(ln: dict) -> str | None:
|
||
"""금액 칸 비목 — 노 · 재 · 경 중 값 하나 · 셋 다 = T · 없음 = None."""
|
||
got = [c for c in "LMS" if (ln["저장"] or {}).get(c)]
|
||
return got[0] if len(got) == 1 else ("T" if got else None)
|
||
|
||
@staticmethod
|
||
def closed(text: str, end: int) -> bool:
|
||
"""칸 나눔 꼴 — 표지 붙은 수 뒤는 닫는 표지 2 칸(빈칸 · 수 끝 점 2 이상 · 줄 끝)."""
|
||
gap = len(text[end:]) - len(text[end:].lstrip(" ."))
|
||
return gap >= 2 or not text[end:].strip()
|
||
|
||
def wrap(self, ln: dict, s: int, e: int, ch: str) -> None:
|
||
ln["_marks"].append((s, e, ch))
|
||
|
||
# ── 자원 찾기 ─────────────────────────────────────────────────
|
||
def names(self, ln: dict, one: dict, start: int = 0) -> int | None:
|
||
"""이름 · 규격 · 단위 자리 표지(`\\x0e` · `\\x13` · `\\x15`) · 끝 자리."""
|
||
text = ln["보임"]
|
||
sp = _span(text, one["이름"], start)
|
||
if not sp:
|
||
return None
|
||
self.wrap(ln, *sp, "\x0e")
|
||
ln["품명"] = text[sp[0] : sp[1]]
|
||
end = sp[1]
|
||
if one.get("규격"):
|
||
m = re.match(r"\s*", text[end:])
|
||
at = end + m.end()
|
||
size = _span(text, one["규격"], at)
|
||
if size and size[0] - at <= 1:
|
||
lo = size[0] - (1 if text[size[0] - 1 : size[0]] in "([" and size[0] > at else 0)
|
||
hi = size[1] + (1 if text[size[1] : size[1] + 1] in ")]" and lo < size[0] else 0)
|
||
self.wrap(ln, lo, hi, "\x13")
|
||
ln["규격"] = text[lo:hi]
|
||
end = hi
|
||
unit = re.match(r"\s*(\[[^\[\]]*\])", text[end:])
|
||
if unit:
|
||
self.wrap(ln, end + unit.start(1), end + unit.end(1), "\x15")
|
||
ln["단위"] = unit.group(1)
|
||
end += unit.end(1)
|
||
return end
|
||
|
||
def find_item(self, ln: dict) -> tuple[str, dict, tuple[int, int, float]] | None:
|
||
"""노임 · 자재 — 글 속 수가 그 단가이고 이름이 그 수 앞에 있음(가장 긴 이름 · 규격 맞음 먼저)."""
|
||
text, body = ln["보임"], sx.flat(ln["보임"])
|
||
best = None
|
||
for hit in _nums(text):
|
||
for kind, one in self.res["_값"].get(round(hit[2], 4), ()):
|
||
name = sx.flat(one["이름"])
|
||
if not name or name not in body:
|
||
continue
|
||
sp = _span(text, one["이름"])
|
||
if not sp or sp[1] > hit[0]:
|
||
continue
|
||
key = (not one["규격"] or sx.flat(one["규격"]) in body, len(name))
|
||
if best is None or key > best[0]:
|
||
best = (key, kind, one, hit)
|
||
return best[1:] if best else None
|
||
|
||
def find_head(self, ln: dict) -> tuple[str, dict] | None:
|
||
"""세 줄 머리 — 「* 단산 N 참조 *」 · 「* 견적 N 참조 *」 · 기계 이름(+규격)."""
|
||
text = ln["보임"]
|
||
m = _REF.search(text)
|
||
if m:
|
||
kind = "산근" if m.group(1) == "단산" else "견적"
|
||
one = self.res["_번"].get((kind, int(m.group(2))))
|
||
if kind == "견적" and (not one or sx.flat(one["이름"]) not in sx.flat(text)):
|
||
body = sx.flat(text[: m.start()]) # 견적 번 ≠ 표 차례 — 이름으로(가장 긴 것)
|
||
hits = [x for x in self.res.get("견적") or [] if sx.flat(x["이름"]) in body]
|
||
one = (
|
||
max(hits, key=lambda x: len(sx.flat(x["이름"]) + sx.flat(x["규격"])))
|
||
if hits
|
||
else None
|
||
)
|
||
return (kind, one) if one else None
|
||
if "=" in text or self.res["_기계틀"] is None or text.lstrip()[:1] in ("※", "*"):
|
||
return None
|
||
body, best = sx.flat(text), None
|
||
for at in self.res["_기계틀"].finditer(body):
|
||
name = at.group(1)
|
||
for one in self.res["_기계"][name]:
|
||
size = sx.flat(one["규격"])
|
||
tail = body[at.start() + len(name) : at.start() + len(name) + len(size) + 2]
|
||
if size and size not in tail:
|
||
continue
|
||
key = len(name) + len(size)
|
||
if best is None or key > best[0]:
|
||
best = (key, one)
|
||
return ("기계", best[1]) if best else None
|
||
|
||
# ── 줄 갈래 ───────────────────────────────────────────────────
|
||
def run(self, src: dict) -> dict[str, Any]:
|
||
ln = self.blank(src)
|
||
text = ln["보임"]
|
||
if not text.strip() and not ln["저장"]:
|
||
ln["연결"] = None
|
||
return self.done(ln)
|
||
heading = bool(_HEADING.match(text))
|
||
part = _label(text)
|
||
if heading:
|
||
ln["단"] = 1
|
||
title = sx.flat(_HEADING.sub("", text))
|
||
if re.search(r"[합소총]계|^계", title):
|
||
self.heads = [] # 합계 절 — 세 줄 머리 닫음
|
||
self.triple = False
|
||
if self.triple and "소계" in sx.flat(text.split(":")[0]) and self.ctx:
|
||
return self.done(self.triple_total(ln))
|
||
self.triple = False
|
||
if part in ("L", "M", "S") and not heading and self.triple_line(ln, part):
|
||
return self.done(ln)
|
||
got = self.find_item(ln)
|
||
total = ln["저장"] if part == "T" else not ln["저장"] # 합계 = 계 줄만 금액 칸
|
||
if part and not heading and got is None and total:
|
||
return self.done(self.total_line(ln, part))
|
||
if got is not None:
|
||
return self.done(self.item(ln, *got))
|
||
if (head := self.find_head(ln)) is not None:
|
||
kind, one = head
|
||
self.link(ln, kind, one)
|
||
end = self.names(ln, one)
|
||
loose = end is None or (one["규격"] and ln["규격"] is None) # 규격이 이름 곁에 없음
|
||
loose = loose or text[end or 0 :].lstrip()[:1] in (":", ":")
|
||
if ln["단"] == 0 and kind == "기계" and loose: # 설명 글 속 장비 이름
|
||
ln |= {"연결": 0, "연결뜻": None, "자원": None, "추정": False, "품명": None}
|
||
ln |= {"규격": None, "단위": None, "_marks": []}
|
||
self.plain(ln)
|
||
return self.done(ln)
|
||
self.heads.append({"종류": kind, "자원": one, "줄": ln, "씀": False})
|
||
self.ctx = self.heads[-1]
|
||
return self.done(ln)
|
||
if self.subtotal(ln) or self.rate62(ln) or self.rate(ln):
|
||
return self.done(ln)
|
||
self.plain(ln)
|
||
return self.done(ln)
|
||
|
||
def sections(self, saved: dict) -> None:
|
||
"""절 소계(가 · 나 … 머리 아래 합계 줄 「노무비 : a + b = X」)가 뺀 줄 하나 = 합에 안 든 줄(비목 없음) —
|
||
절(앞 숫자 머리부터) 안 그 비목 줄 결과 합 − X 가 꼭 한 줄 값이고 나머지 줄 값은 모두 덧셈 글에 있을
|
||
때만 · 블록 금액도 그 줄을 빼야 맞을 때만(엑셀 글 덧셈 · 원파일 SCheck 0)."""
|
||
lines, start = self.lines, 0
|
||
for i, ln in enumerate(lines):
|
||
text = ln["보임"]
|
||
if ln["단"] == 1 and re.match(r"^\s*\d", text):
|
||
start = i
|
||
if not (ln["합계"] and ln["비목"] in ("L", "M", "S") and ln["결과"] and "+" in text):
|
||
continue
|
||
head = next((k for k in range(i - 1, start - 1, -1) if lines[k]["단"] == 1), None)
|
||
if head is None or not re.match(r"^\s*[가-하]", lines[head]["보임"]):
|
||
continue
|
||
part, want = ln["비목"], ln["결과"][-1]
|
||
kids = [
|
||
x for x in lines[start:head] if x["비목"] == part and x["결과"] and not x["합계"]
|
||
]
|
||
kids += [
|
||
x for x in lines[start:head] if x["비목"] == part and x["줄참조"] and not x["결과"]
|
||
]
|
||
value = {id(x): (x["결과"] or x["줄참조"])[-1] for x in kids}
|
||
extra = sum(value.values()) - want
|
||
odd = [x for x in kids if abs(value[id(x)] - extra) < 0.5]
|
||
colon = _COLON.search(text)
|
||
adds = [v for _, _, v in _nums(text, colon.end() if colon else 0, text.rfind("="))]
|
||
rest = [value[id(x)] for x in kids if odd and x is not odd[0]]
|
||
listed = all(any(_close(v, a) for a in adds) for v in rest)
|
||
if len(odd) != 1 or abs(extra) < 0.5 or not listed:
|
||
continue
|
||
every = sum(
|
||
(x["결과"] or x["줄참조"])[-1]
|
||
for x in lines
|
||
if x["비목"] == part and not x["합계"] and (x["결과"] or x["줄참조"])
|
||
)
|
||
block = float(saved.get(part) or 0) # 블록 금액이 그 줄을 빼야 맞을 때만
|
||
if abs(every - block) >= 1 and abs(every - extra - block) < 1:
|
||
odd[0]["비목"] = None
|
||
|
||
def unused(self) -> None:
|
||
"""머리 아닌 줄의 기계 이름 — 아래 세 줄이 안 쓰면 글 줄(설명 속 장비 이름)."""
|
||
for h in self.heads:
|
||
ln = h["줄"]
|
||
if h["종류"] == "기계" and not h["씀"] and ln["단"] == 0:
|
||
ln |= {"연결": 0, "연결뜻": None, "자원": None, "추정": False, "품명": None}
|
||
ln |= {"규격": None, "단위": None, "_marks": []}
|
||
|
||
def done(self, ln: dict) -> dict[str, Any]:
|
||
self.lines.append(ln)
|
||
return ln
|
||
|
||
def total_line(self, ln: dict, part: str) -> dict:
|
||
"""합계 줄 — 「노무비 : 식 = 값」 · 「재료비 : 값」 · 「계 : 값」(계 = 결과)."""
|
||
text = ln["보임"]
|
||
ln |= {"합계": True, "비목": part, "단": -1}
|
||
lead = len(text) - len(text.lstrip())
|
||
res = _result(text)
|
||
if res:
|
||
self.wrap(ln, lead, text.rfind("=") + 1, "\x1a")
|
||
self.result(ln, res)
|
||
elif part == "T":
|
||
colon = _COLON.search(text).end()
|
||
self.wrap(ln, lead, colon, "\x1a")
|
||
nums = _nums(text, colon)
|
||
if nums:
|
||
self.wrap(ln, nums[0][0], nums[0][1], "\x12")
|
||
ln["결과"] = [nums[0][2]]
|
||
else:
|
||
self.wrap(ln, lead, len(text.rstrip()), "\x1a")
|
||
return ln
|
||
|
||
def result(self, ln: dict, res: tuple) -> None:
|
||
s, e, v, letter = res
|
||
if letter is not None:
|
||
self.wrap(ln, letter, letter + 1, "\x06")
|
||
self.wrap(ln, s, e, "\x12")
|
||
ln["결과"].append(v)
|
||
|
||
def triple_line(self, ln: dict, part: str) -> bool:
|
||
"""세 줄 — 단가 = 머리 자원(기계 · 산근 · 견적)의 그 비목 값."""
|
||
text = ln["보임"]
|
||
colon = _COLON.search(text).end()
|
||
nums = _nums(text, colon)
|
||
if not nums or not self.heads:
|
||
return False
|
||
price = nums[0]
|
||
tail = text[price[1] :]
|
||
if ("=" not in tail and tail.strip()) or tail.lstrip()[:1] == "+":
|
||
return False
|
||
fit = [
|
||
h
|
||
for h in reversed(self.heads)
|
||
if _close(price[2], float(h["자원"]["값"].get(part) or 0))
|
||
]
|
||
if not fit:
|
||
return False
|
||
self.ctx = fit[0]
|
||
self.ctx["씀"] = True
|
||
one = self.ctx["자원"]
|
||
self.link(ln, self.ctx["종류"], one)
|
||
ln["비목"] = part
|
||
if self.ctx["종류"] == "기계":
|
||
ln["기계"] = True
|
||
self.money(ln, price)
|
||
self.triple = True
|
||
return True
|
||
|
||
def triple_total(self, ln: dict) -> dict:
|
||
one = self.ctx["자원"]
|
||
self.link(ln, self.ctx["종류"], one)
|
||
ln["비목"] = "T"
|
||
text = ln["보임"]
|
||
res = _result(text)
|
||
if res:
|
||
self.result(ln, res)
|
||
else:
|
||
m = re.search(r"[::]\s*(?:([A-Z])\s*)?(" + NUM + r")", text)
|
||
if m:
|
||
self.result(
|
||
ln, (m.start(2), m.end(2), _v(m.group(2)), m.start(1) if m.group(1) else None)
|
||
)
|
||
money = ln["저장"] or {}
|
||
rows = []
|
||
for one in reversed(self.lines):
|
||
if one["연결"] != ln["연결"] or one["비목"] not in ("L", "M", "S"):
|
||
break
|
||
rows.append(one)
|
||
cols = [c for c in "LMS" if money.get(c)]
|
||
total = sum((one["결과"] or [0])[-1] for one in rows)
|
||
if len(cols) == 1 and money.get("T") and abs(total - money["T"]) < 1:
|
||
for one in rows: # 계 줄 금액 칸 비목 하나 = 값 있는 세 줄 모두 그 비목으로 옮김
|
||
if (one["결과"] or [0])[-1]:
|
||
one["비목"] = cols[0]
|
||
elif money:
|
||
for one in rows: # 계 줄 금액 칸이 빈 비목 = 합에 안 든 줄(비목 없음)
|
||
if not money.get(one["비목"]) and one["결과"] and one["결과"][-1]:
|
||
one["비목"] = None
|
||
self.triple = False
|
||
return ln
|
||
|
||
def item(self, ln: dict, kind: str, one: dict, price: tuple) -> dict:
|
||
self.link(ln, kind, one)
|
||
self.names(ln, one)
|
||
part = self.money_part(ln)
|
||
ln["비목"] = part if part in ("L", "M", "S") else None
|
||
self.money(ln, price)
|
||
return ln
|
||
|
||
def money(self, ln: dict, price: tuple[int, int, float]) -> None:
|
||
"""금액 줄 — 단가(`\\x03`) · 첫 식 조각의 앞 대입 값 = 변수(`\\x11`) · 끝 = 결과."""
|
||
text = ln["보임"]
|
||
s, e, v = price
|
||
self.wrap(ln, s, e, "\x03")
|
||
ln["연결단가"] = [v]
|
||
self.var(ln, e)
|
||
res = _result(text)
|
||
if res and res[0] > e:
|
||
self.result(ln, res)
|
||
|
||
def var(self, ln: dict, start: int) -> None:
|
||
"""첫 식 조각(start ~ 첫 「=」)의 수 중 가장 늦게 대입한 변수 값 하나 = 변수(`\\x11` · 원파일 = 줄마다 하나) ·
|
||
칸 나눔 꼴은 닫는 표지 자리(2 칸)가 있는 수만."""
|
||
text = ln["보임"]
|
||
eq = text.find("=", start)
|
||
order = list(self.vars.values())
|
||
best = None
|
||
for a, b, val in _nums(text, start, eq if eq >= 0 else None):
|
||
rank = max((i for i, x in enumerate(order) if _close(val, x)), default=-1)
|
||
if ln["_칸"] and not self.closed(text, b):
|
||
continue
|
||
if rank >= 0 and (best is None or rank > best[0]):
|
||
best = (rank, a, b, val)
|
||
if best:
|
||
self.wrap(ln, best[1], best[2], "")
|
||
ln["변수"].append(best[3])
|
||
|
||
def subtotal(self, ln: dict) -> bool:
|
||
"""「…소계 : 수」 — 금액 칸 계 없음 · 비목 하나(61) · 금액 칸 없음 = 비목 없음."""
|
||
text = ln["보임"]
|
||
m = _COLON.search(text)
|
||
if not m or "소계" not in sx.flat(text[: m.start()]):
|
||
return False
|
||
money = ln["저장"] or {}
|
||
parts = [c for c in "LMS" if money.get(c)]
|
||
if len(parts) != 1 or (
|
||
money.get("T") is not None and not _close(money["T"], money[parts[0]])
|
||
):
|
||
return False # 소계 61 = 금액 칸 비목 하나(계 칸 없음 · 있으면 같은 값)
|
||
got = re.match(r"\s*(?:([A-Z])\s*)?(" + NUM + r")\.?(?![\d,])", text[m.end() :])
|
||
if not got:
|
||
return False
|
||
at = m.end()
|
||
if got.group(1):
|
||
self.wrap(ln, at + got.start(1), at + got.end(1), "\x06")
|
||
self.wrap(ln, at + got.start(2), at + got.end(2), "\x0c")
|
||
ln["줄참조"] = [_v(got.group(2))]
|
||
ln["저장"] = {**money, "T": money[parts[0]]}
|
||
self.link(ln, "소계", None)
|
||
ln["비목"] = self.money_part(ln)
|
||
ln["표지"] = "소계"
|
||
self.var(ln, at + got.end(2))
|
||
res = _result(text)
|
||
if res and res[0] > at + got.end(2):
|
||
self.result(ln, res)
|
||
return True
|
||
|
||
def rate62(self, ln: dict) -> bool:
|
||
"""「(A 수 + 수) × n% = 값」 · 「A 수 + 수 = 합 × n% = 값」 — 구간 합 율(62 · 합계 표지)."""
|
||
text = ln["보임"]
|
||
m = _GROUP62.search(text)
|
||
if not m or not re.search(r"[×xX*]", text[m.end() :]) or not ln["저장"]:
|
||
return False
|
||
self.wrap(ln, m.start(1), m.end(1), "\x07")
|
||
self.wrap(ln, m.start(2), m.end(3), "\x1a")
|
||
ln |= {"합계": True}
|
||
self.link(ln, "율62", None)
|
||
ln["비목"] = self.money_part(ln)
|
||
ln["표지"] = "율"
|
||
for r in re.finditer(r"=\s*(?:([A-Z])\s*)?(" + NUM + r")\.?(?![\d,])", text):
|
||
self.result(
|
||
ln, (r.start(2), r.end(2), _v(r.group(2)), r.start(1) if r.group(1) else None)
|
||
)
|
||
return True
|
||
|
||
def rate(self, ln: dict) -> bool:
|
||
"""「수 × n% = 값」 + 금액 칸 — 율(58 · 첫 수 = 줄참조)."""
|
||
text = ln["보임"]
|
||
res = _result(text)
|
||
if not ln["저장"] or not res:
|
||
return False
|
||
colon = _COLON.search(text)
|
||
nums = _nums(text, colon.end() if colon else 0, text.rfind("="))
|
||
seen = [v for x in self.lines for v in x["결과"] + x["줄참조"]]
|
||
before = any(_close(nums[0][2], v) for v in seen) if nums else False
|
||
lead = text[colon.end() if colon else 0 :].lstrip(" (")
|
||
before = before and lead[:1].isdigit()
|
||
if not nums or ("%" not in text[: text.rfind("=")] and not before):
|
||
return False
|
||
self.wrap(ln, nums[0][0], nums[0][1], "\x0c")
|
||
ln["줄참조"] = [nums[0][2]]
|
||
self.link(ln, "율", None)
|
||
ln["비목"] = self.money_part(ln)
|
||
ln["표지"] = "율"
|
||
self.result(ln, res)
|
||
return True
|
||
|
||
def plain(self, ln: dict) -> None:
|
||
"""글 · 변수 줄 — 대입마다 「이름 = 식 = 값」 = 결과 · 그 밖 「… = 값」(식 있는 줄) = 결과."""
|
||
text = ln["보임"]
|
||
ln["연결"] = 0
|
||
part = self.money_part(ln)
|
||
ln["비목"] = part
|
||
hits: list[re.Match] = []
|
||
for m in _ASSIGN.finditer(text):
|
||
if m.group(1).lower() in _UNITS and re.search(r"\d\.?\s?$", text[: m.start()]):
|
||
continue # 「8 hr =」 = 단위
|
||
gap = text[hits[-1].end() : m.start()] if hits else ""
|
||
if not hits or (re.search(r"\d", gap) and not re.search(r"[×÷+\-*/(=]\s*$", gap)):
|
||
hits.append(m)
|
||
lead = text[: hits[0].start()].strip(" ○●·-{") if hits else ""
|
||
if hits and (not lead or re.search(r"[::]$", lead)):
|
||
for n, m in enumerate(hits):
|
||
end = hits[n + 1].start() if n + 1 < len(hits) else len(text)
|
||
seg = text[m.end() : end]
|
||
parts = seg.split("=")
|
||
val = re.match(r"\s*(?:[A-Z](?=\d))?(" + NUM + r")", parts[-1])
|
||
if not val:
|
||
continue
|
||
at = m.end() + len(seg) - len(parts[-1])
|
||
if len(parts) >= 2:
|
||
res = _result(text[:end])
|
||
if res and res[0] >= at:
|
||
self.result(ln, res)
|
||
self.vars.pop(m.group(1), None)
|
||
self.vars[m.group(1)] = _v(val.group(1))
|
||
return
|
||
eq = text.rfind("=")
|
||
seg = re.split(r"[=::]", text[:eq])[-1]
|
||
if eq > 0 and re.search(r"[×÷+*]|\d\s*/|/\s*\d", seg):
|
||
res = _result(text)
|
||
if res:
|
||
self.result(ln, res)
|
||
|
||
|
||
# ── 책 ────────────────────────────────────────────────────────────────
|
||
def _glyph(text: str, marks: list[tuple[int, int, str]]) -> str:
|
||
"""보인 글 + 표지 자리 → 원파일 LineData 꼴(닫는 표지 먼저)."""
|
||
ins: list[tuple[int, int, str]] = []
|
||
for s, e, ch in marks:
|
||
ins.append((s, 1, ch))
|
||
ins.append((e, 0, ch))
|
||
out, last = [], 0
|
||
for at, _, ch in sorted(ins, key=lambda x: (x[0], x[1])):
|
||
out.append(text[last:at])
|
||
out.append(ch)
|
||
last = at
|
||
out.append(text[last:])
|
||
return "".join(out)
|
||
|
||
|
||
def index(res: dict[str, list[dict]]) -> dict[str, Any]:
|
||
"""자원 표 → 찾기 색인(단가 → 노임 · 자재 · 이름 → 기계 · (종류, 번) → 산근 · 견적)."""
|
||
out: dict[str, Any] = dict(res)
|
||
out["_값"] = {}
|
||
for kind in ("노임", "자재"):
|
||
for one in res.get(kind) or []:
|
||
out["_값"].setdefault(round(one["값"]["T"], 4), []).append((kind, one))
|
||
out["_기계"] = {}
|
||
for one in res.get("기계") or []:
|
||
if sx.flat(one["이름"]):
|
||
out["_기계"].setdefault(sx.flat(one["이름"]), []).append(one)
|
||
names = sorted(out["_기계"], key=len, reverse=True)
|
||
out["_기계틀"] = re.compile("(?=(" + "|".join(map(re.escape, names)) + "))") if names else None
|
||
out["_번"] = {(k, one["번"]): one for k in ("산근", "견적") for one in res.get(k) or []}
|
||
return out
|
||
|
||
|
||
def read(sheets: dict[str, list]) -> dict[str, Any]:
|
||
"""EST 엑셀 시트 → {꼴, 산근, 일위, 자원, 내역, 표지}(`M02_San_Estx.read` 꼴 · 일위 = 호표 길)."""
|
||
res = sx.resources(sheets)
|
||
look = index(res)
|
||
total = {one["번"]: one for one in res.get("산근") or []}
|
||
out: dict[str, Any] = {"꼴": "EST", "산근": [], "일위": [], "내역": None}
|
||
for b in blocks(sheets.get(SHEET) or []):
|
||
tags: list[str] = []
|
||
one = total.get(b["번"])
|
||
m = _HEAD.match(b["머리"])
|
||
head = {
|
||
"코드": f"단산{b['번']}",
|
||
"이름": one["이름"] if one else m.group(2).strip(),
|
||
"규격": one["규격"] if one else "",
|
||
"단위": one["단위"] if one else m.group(3).strip(),
|
||
"저장": b["저장"] or (dict(one["값"]) if one else None),
|
||
}
|
||
if not one:
|
||
tags.append(f"{sx.NOTE}총괄표에 단산{b['번']} 없음")
|
||
elif one.get("칸"): # 총괄표 당초 / 적용 칸(797-1a)
|
||
tags.append(f"참고 · {one['칸']}")
|
||
work = Lines(look, tags)
|
||
for src in b["줄"]:
|
||
work.run(src)
|
||
work.unused()
|
||
work.sections(head["저장"] or {})
|
||
lines = []
|
||
for ln in work.lines:
|
||
marks = ln.pop("_marks")
|
||
ln.pop("_칸", None)
|
||
ln["글"] = _glyph(ln["보임"], marks)
|
||
lines.append(ln)
|
||
out["산근"].append({**head, "번": b["번"], "엑셀행": b["행"], "줄": lines, "표지": tags})
|
||
out["자원"] = {
|
||
f"{kind}:{one['번']}": {k: one[k] for k in ("이름", "규격", "단위", "표")}
|
||
| {"저장": one["값"]}
|
||
for kind, rows in res.items()
|
||
for one in rows
|
||
}
|
||
out["표지"] = [
|
||
f"{sx.NOTE}자원 코드 · LinkIndex · 자리(Diget) 없음 — 연결뜻 = 글 · 금액 칸 · 단가표로 정함({sx.GUESS})",
|
||
f"{sx.NOTE}자원 코드 = 단가표 이름#No(산근 = 단산 번)",
|
||
]
|
||
return out
|