- 새 M02_Doc_NativeEst(EST 산근 블록 → 가져온 로직 · 단가 \x03 · 변수 \x11 · 줄참조 \x0c 표지로 정함 · 변수 이름 그대로 · 분수 그림 공식 = 위아래 줄 칸 · 기계 · 산근 · 견적 세 줄 = 로직 값묶음 한 줄 · 소계 61 = 합 밖 · 율 58 · 62 = 덧줄 · 못 읽은 줄 = 보인 값 얼림 + 참고 표지 · 품셈 조건 글 = 추정 표지) - 새 M02_Doc_FromNative(원파일 읽기 · 둘 꼴 · 블록 번 = 엑셀 길과 같음 · STmate = 원문 그대로 M02_San_Emit.stmate · 같은 공사 판정 · EST 일위대가 → 실무자료 호표 꼴) - M02_Sangeun_FromHopyo — 산근 자리 칸 원파일 · 원파일 우선 emit · 안쪽 산근 원파일 코드로 · upload_native(같은 공사 엑셀 HP 에 붙음 · 없으면 새 HP) - M02_Hopyo_Router 올림 — .estx · .stc 이면 upload_native(한 덩이) - M02_Doc_Import — 원파일 문서 소계 덧줄 비목 칸 비움 · 수 하나 변수 출처 원파일 · 머리 원파일 - 수식 문서 셈 짝(master_doc · master_doc_calc ↔ common_util_formula_doc · _calc) — 합밖 + 비목 비움 = 금액만(소계 보임) · 그 밖 그대로 - M02_San_Estx 구조 줄 색인(Index) 칸 · 어디서 왔나 카드 「EST · 원파일」 - 전수 대조 — EST 산근 1,849 · STC 371 블록 수식 문서 셈 = 원파일 저장값(STC 계약단가 82 = 낙찰률 버림 뒤) · 표지 없는 틀린 줄 0 · 목록 docs/raw/요구_1104/_목록_810-2_원파일변환.xlsx - 시험 test_810_2_native_doc(전수 · 꼴 · 문서 셈 거울 · 올림 분기) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
571 lines
29 KiB
Python
571 lines
29 KiB
Python
"""EST 원파일 산근 블록 → 가져온 로직 줄(PLAN 810-2 · 요구 705 · 706 · 서버 단독 · 읽기만).
|
||
|
||
바탕 = `M02_San_Estx.read` 구조 줄(칸 뜻 `resources/master_data/_원파일파서_틀.md`) — 엑셀 글 추정 대신
|
||
원파일 표지로 정함(`M02_San_Est` 값 역색인 안 씀):
|
||
단가 = `\\x03` 수 + 자원 코드(LinkIndex) · 변수 = `\\x11` 수(앞 변수 줄 값) · 줄참조 = `\\x0c` 수 ·
|
||
결과 = `\\x12` 수 · 비목 = SCheck(음수 = 그 줄이 냄 · 율 · 소계 양수 = 대상).
|
||
꼴 = 793-2a 내보내기(`M02_San_Emit.Out`) 그대로 → `M02_Sangeun_Imported.create` · `M02_Doc_Import.derive`
|
||
가 수식 문서로(엑셀 길과 같은 길 · 같은 연결 규칙 `M02_San_Emit._m01` · 797-1 기계 시간당 이음).
|
||
변수 대입 줄 = 중간(변수 그대로 · 공식 글 · 분수 그림 ─ 은 위아래 줄 칸 · 결과와 맞을 때만 식 ·
|
||
아니면 결과 값 + 표지) · 자원 줄 = 호표(단가 = 원파일 값 · 수량 = 식 · 변수 = 이름) ·
|
||
기계 · 산근 · 견적 세 줄(같은 코드 · 노 · 재 · 경 · 같은 수량 식) = 로직 값묶음 한 줄(소계 줄 = 글) ·
|
||
소계(61) = 합 밖 덧줄(출처 `SUB` · 문서에서 비목 칸 비움) + 아래 줄이 그 비목으로 합에 듦 ·
|
||
율(58 · 62) = 덧줄(줄참조 = 앞 줄 부름 · 구간 `\\x07X\\x07` = `\\x06X\\x06` 줄 합) ·
|
||
합계(\\x1a) = 글(블록 합 = 문서 비목 합 · 끝수 버림 0).
|
||
못 읽은 줄 = 보인 결과 값 얼림 + 「참고 · …」 표지(`NOTE`) — 지어 넣는 값 없음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from M02_MasterTemplete import M02_San_Emit as em
|
||
from M02_MasterTemplete import M02_San_Est as est
|
||
from M02_MasterTemplete import M02_San_Expr as ex
|
||
|
||
COMP = em.COMP
|
||
NOTE = em.NOTE
|
||
SUB = "원파일 소계" # 덧줄 출처 — 합 밖 소계(문서 비목 칸 비움 · `M02_Doc_Import`)
|
||
TRIPLE = (101, 104, 105) # 견적 · 산근 · 기계 — 노 · 재 · 경 세 줄
|
||
GALLAE = {"노임": "인력", "자재": "재료", "기계": "기계", "견적": "고정", "산근": "단가산출"}
|
||
GALLAE["표준"] = "고정"
|
||
NATURAL = {"노임": "L", "자재": "M"}
|
||
_SENT = 7_770_000.0 # 변수 이름 자리 표 수(토큰 읽기용 · 셈 전에 값으로 바꿈)
|
||
_ASSIGN = re.compile(r"(?<![A-Za-z0-9_])([A-Za-z][A-Za-z0-9]*)\s*(?:\([^)=]*\))?\s*=(?!=)")
|
||
_DROP = re.compile(r"\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\x06[A-Z]\x06")
|
||
_GROUP = re.compile(r"\x07([A-Z])\x07\s*\x1a[^\x1a]*\x1a")
|
||
_CTRL = re.compile(r"[\x00-\x1f]")
|
||
GUESS = "추정"
|
||
_COND = re.compile(r"품셈|기준|적용|이상|이하|미만|초과")
|
||
|
||
|
||
def _clean(raw: str) -> str:
|
||
return _CTRL.sub("", _DROP.sub(" ", raw or ""))
|
||
|
||
|
||
def _close(a: float, b: float) -> bool:
|
||
return abs(a - b) < 1e-6 * max(1.0, abs(b))
|
||
|
||
|
||
def _fit(got: float, want: float) -> str | None:
|
||
"""보인 값이 되는 자리 규칙 — `M02_San_Expr.fit`(3 자리까지) + 버림 · 반올림 4~6 자리(EST Diget)."""
|
||
if rule := ex.fit(got, want):
|
||
return rule
|
||
for n in (4, 5, 6):
|
||
q = 10**n
|
||
if abs(ex.math.floor(round(got * q, 6)) / q - want) < 1e-9:
|
||
return f"버림{n}"
|
||
if abs(ex.math.floor(got * q + 0.5 + 1e-9) / q - want) < 1e-9:
|
||
return f"반올림{n}"
|
||
return None
|
||
|
||
|
||
class Block:
|
||
"""EST 산근 블록 한 벌 → `{로직, 원본}`."""
|
||
|
||
def __init__(self, blk: dict, no: int, keys: bool, file: str, names: dict[str, int]) -> None:
|
||
name = str(blk.get("이름") or blk.get("코드") or "")
|
||
if blk.get("규격"):
|
||
name = f"{name}({blk['규격']})"
|
||
self.o = em.Out(name, blk.get("단위"), "EST", no)
|
||
self.o.head |= {"원파일": blk.get("코드")}
|
||
self.blk, self.keys, self.file, self.nos = blk, keys, file, names
|
||
self.lines: list[dict] = blk.get("줄") or []
|
||
self.vars: dict[str, tuple[str, float]] = {} # 원 이름 → (엔진 이름, 값)
|
||
self.done: list[tuple[float, str, dict]] = [] # (결과, 부름 글, 줄) — 줄참조 찾기
|
||
self.groups: dict[str, list[tuple[str, float]]] = {} # 구간 글자 → (부름 글, 결과)
|
||
self.extra: dict[int, dict] = {} # 행 → 소계가 정한 비목(아래 줄)
|
||
self.got = {"확정": 0, "표지": 0, "추정": 0}
|
||
|
||
# ── 작은 도구 ──────────────────────────────────────────────────
|
||
def mark(self, ln: dict, why: str) -> None:
|
||
self.o.glyph(ln["행"], ln.get("보임") or "", mark=f"{NOTE}{why}")
|
||
self.got["표지"] += 1
|
||
|
||
def _sub(self, seg: str) -> tuple[str, dict[float, str]]:
|
||
"""식 조각 속 아는 변수 이름 → 표 수(뒤에서 이름으로 바꿈)."""
|
||
out, k = {}, 0
|
||
for name in sorted(self.vars, key=len, reverse=True):
|
||
pat = re.compile(rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])")
|
||
if pat.search(seg):
|
||
k += 1
|
||
mark = _SENT + k
|
||
out[mark] = name
|
||
seg = pat.sub(em.num(mark), seg)
|
||
return seg, out
|
||
|
||
def formula(
|
||
self, seg: str, want: float | None, extra: dict[float, float] | None = None
|
||
) -> tuple[tuple, dict, str | None] | None:
|
||
"""식 조각 → (나무, {토큰 차례: 변수 이름}, 자리 규칙) · 결과와 맞지 않으면 None ·
|
||
extra = {표 수: 값}(구간 합 자리)."""
|
||
seg, names = self._sub(_power(seg.replace("·", "×")))
|
||
toks = est.tokens(seg)
|
||
if not toks:
|
||
return None
|
||
try:
|
||
tree = ex.parse(toks)
|
||
except ex.ExprError:
|
||
return None
|
||
vars_ = {n[2]: names[n[1]] for n in ex.numbers(tree) if n[1] in names}
|
||
sub = {i: self.vars[v][1] for i, v in vars_.items()}
|
||
extra = extra or {}
|
||
sub |= {n[2]: extra[n[1]] for n in ex.numbers(tree) if n[1] in extra}
|
||
try:
|
||
got = ex.value(tree, {"V": {}, "sub": sub})
|
||
except ex.ExprError:
|
||
return None
|
||
same = want is None or abs(got - want) <= 1e-9 * max(1.0, abs(want))
|
||
rule = "그대로" if same else _fit(got, want)
|
||
return (tree, vars_, rule) if rule else None
|
||
|
||
def ref_of(self, value: float, before: int) -> str | None:
|
||
"""줄참조 수 → 앞 줄 부름(가장 가까운 같은 결과)."""
|
||
for v, call, ln in reversed(self.done):
|
||
if ln["행"] < before and _close(v, value):
|
||
return call
|
||
return None
|
||
|
||
def var_of(self, value: float) -> str | None:
|
||
for name, (_, v) in reversed(list(self.vars.items())):
|
||
if _close(v, value):
|
||
return name
|
||
return None
|
||
|
||
# ── 변수 줄 ────────────────────────────────────────────────────
|
||
def assigns(self, i: int, ln: dict) -> bool:
|
||
"""「q = 0.7 K = 0.55 f = 1 / 1.325 = 0.75」 · 분수 그림 Q = ── = ── = 23.39 → 중간."""
|
||
text = _clean(ln.get("글") or "")
|
||
hits: list[re.Match] = []
|
||
for m in _ASSIGN.finditer(text): # 앞 대입 값이 끝난 뒤(연산 · = 뒤 아님)만 새 대입
|
||
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 not hits or (lead and not re.search(r"[::]$", lead)):
|
||
return False
|
||
for n, m in enumerate(hits):
|
||
end = hits[n + 1].start() if n + 1 < len(hits) else len(text)
|
||
self.assign(i, ln, m.group(1), text[m.end() : end])
|
||
return True
|
||
|
||
def assign(self, i: int, ln: dict, name: str, rhs: str) -> None:
|
||
parts = rhs.split("=")
|
||
lead = re.match(r"\s*(" + est.NUMRE + r")", parts[-1])
|
||
if not lead:
|
||
return
|
||
value = est.num(lead.group(1))
|
||
body = None
|
||
for seg in parts[:-1]:
|
||
if "─" in seg:
|
||
seg = self.fraction(i, seg)
|
||
got = self.formula(seg, value) if seg else None
|
||
if got:
|
||
body = self.text(got)
|
||
break
|
||
eng = self.o.names.line(name)
|
||
if body is None:
|
||
body = em.num(value)
|
||
if len(parts) > 1:
|
||
self.mark(ln, f"변수 {name} 공식 글 못 읽음 — 결과 값 {em.num(value)}")
|
||
self.vars[name] = (eng, value)
|
||
self.o.middle(eng, body, None, ln["행"], ln.get("보임") or "")
|
||
self.got["확정"] += 1
|
||
|
||
def fraction(self, i: int, seg: str) -> str:
|
||
"""분수 그림 ─ = 위 줄 · 아래 줄 같은 차례 칸(node `native-text` 와 같은 읽기)."""
|
||
|
||
def cols(k: int) -> list[str]:
|
||
if not 0 <= k < len(self.lines):
|
||
return []
|
||
t = _clean(self.lines[k].get("글") or "")
|
||
return re.sub(r"\s*([×÷·+*/−-])\s*", r"\1", t).strip().split()
|
||
|
||
tops, bottoms, at = cols(i - 1), cols(i + 1), [0]
|
||
|
||
def one(_: re.Match) -> str:
|
||
k = at[0]
|
||
at[0] += 1
|
||
top = tops[k] if k < len(tops) else ""
|
||
low = bottoms[k] if k < len(bottoms) else ""
|
||
return f"({top})/({low})" if top and low else "?"
|
||
|
||
return re.sub(r"─+", one, seg)
|
||
|
||
def text(self, got: tuple, swap: dict[int, str] | None = None) -> str:
|
||
"""식 글 — 자리 규칙(그대로 빼고)을 식 안 끝수로(금액 줄은 `money` 가 버림1 을 넘김 = 줄 끝수)."""
|
||
tree, vars_, rule = got
|
||
swap = {**{i: self.vars[v][0] for i, v in vars_.items()}, **(swap or {})}
|
||
body = em.text(tree, {}, swap)
|
||
m = re.fullmatch(r"(버림|반올림|올림)(\d)", rule or "")
|
||
if m:
|
||
body = f"{m.group(1)}({body}, {m.group(2)})"
|
||
return body
|
||
|
||
# ── 금액 줄 ────────────────────────────────────────────────────
|
||
def money(self, ln: dict) -> tuple[str, dict | None] | None:
|
||
"""금액 줄 식 → (식 글(단가 자리 = 1 · 변수 = 이름 · 줄참조 = 부름), 자리) · 못 읽음 None."""
|
||
raw = ln.get("글") or ""
|
||
group: dict[float, str] = {}
|
||
sums: dict[float, float] = {}
|
||
for k, m in enumerate(_GROUP.finditer(raw)):
|
||
calls = self.groups.get(m.group(1)) or []
|
||
if not calls:
|
||
return None
|
||
mark = _SENT + 500 + k
|
||
group[mark] = "(" + " + ".join(c for c, _ in calls) + ")"
|
||
sums[mark] = sum(v for _, v in calls)
|
||
raw = raw.replace(m.group(0), f" {em.num(mark)} ", 1)
|
||
text = _clean(raw)
|
||
head = text.split("=", 1)[0]
|
||
if ":" in head:
|
||
text = text.split(":", 1)[1]
|
||
parts = text.split("=")
|
||
want = ln["결과"][-1] if ln.get("결과") else None
|
||
if want is None or len(parts) < 2:
|
||
return None
|
||
price = (ln.get("연결단가") or [None])[0]
|
||
for seg in parts[:-1]:
|
||
bare = re.fullmatch(r"\s*(" + est.NUMRE + r")\s*", seg)
|
||
if bare and price is not None and _close(est.num(bare.group(1)), want):
|
||
return "1", None # 단가 그대로(× 1)
|
||
got = self.formula(seg, want, sums)
|
||
if not got:
|
||
continue
|
||
swap: dict[int, str] = {}
|
||
used_price = False
|
||
for _, v, idx in ex.numbers(got[0]):
|
||
if idx in got[1]:
|
||
continue
|
||
if v in group:
|
||
swap[idx] = group[v]
|
||
elif price is not None and not used_price and _close(v, price):
|
||
swap[idx], used_price = "1", True
|
||
elif v in (ln.get("변수") or []) and self.var_of(v):
|
||
swap[idx] = self.vars[self.var_of(v)][0]
|
||
elif v in (ln.get("줄참조") or []) and (call := self.ref_of(v, ln["행"])):
|
||
swap[idx] = call
|
||
if price is not None and not used_price:
|
||
continue
|
||
place = em._place(got[2]) if got[2] != "그대로" else None
|
||
return self.text((got[0], got[1], None), swap), place
|
||
return None
|
||
|
||
def frozen(self, ln: dict, why: str) -> tuple[str, dict | None]:
|
||
self.mark(ln, f"{why} — 보인 값 {em.num(ln['결과'][-1])} 얼림")
|
||
return em.num(ln["결과"][-1]), None
|
||
|
||
def res(self, ln: dict, part: str | None, price: float | None) -> dict:
|
||
r = ln.get("자원") or {}
|
||
kind = r.get("종류")
|
||
out = {"꼴": "EST", "갈래": GALLAE.get(kind, "고정"), "종류": kind, "코드": r.get("코드")}
|
||
out |= {"이름": r.get("이름"), "규격": r.get("규격") or "", "단위": r.get("단위")}
|
||
if kind == "산근" and r.get("코드") in self.nos:
|
||
out["이름"] = f"단산{self.nos[r['코드']]}" # 같은 파일 안쪽 블록(797-1 형제 SK)
|
||
out["원이름"] = r.get("이름")
|
||
if part:
|
||
nat = NATURAL.get(kind, part)
|
||
out |= {"단가": {nat: price}, "비목": nat, "열쇠비목": nat}
|
||
out["옮김"] = part if part != nat else None
|
||
return out
|
||
|
||
def register(self, ln: dict, call: str) -> None:
|
||
if ln.get("결과"):
|
||
self.done.append((ln["결과"][-1], call, ln))
|
||
g = re.search(r"\x06([A-Z])\x06", ln.get("글") or "")
|
||
if g:
|
||
self.groups.setdefault(g.group(1), []).append((call, (ln.get("결과") or [0.0])[-1]))
|
||
|
||
def item(self, ln: dict, part: str | None, inside: bool) -> None:
|
||
"""자원 줄(노임 · 자재 · 표준 · 견적) → 호표 단가 줄."""
|
||
price = (ln.get("연결단가") or [None])[0]
|
||
got = self.money(ln) if price is not None else None
|
||
qty, place = got if got else ("1", self.frozen(ln, "수량 식 못 읽음")[1])
|
||
comp = part or NATURAL.get((ln.get("자원") or {}).get("종류")) or "S"
|
||
r = self.res(ln, comp, price if got else ln["결과"][-1])
|
||
name = self.o.names.item(ln.get("품명") or r.get("이름") or "줄")
|
||
link, many = em._m01(r["갈래"], r) if self.keys else (None, None)
|
||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||
one |= {} if inside else {"합밖": True}
|
||
one |= {"자리": place} if place else {}
|
||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||
if many:
|
||
self.mark(ln, many.removeprefix(NOTE))
|
||
self.got["확정"] += bool(got)
|
||
self.register(ln, f"줄.'{name}'")
|
||
|
||
def triple(self, rows: list[dict], total: dict | None) -> None:
|
||
"""기계 · 산근 · 견적 세 줄 → 로직 값묶음 한 줄(수량 식 · 자리 같을 때) · 아니면 줄마다."""
|
||
got = [self.money(ln) for ln in rows]
|
||
parts = [ln["비목"] for ln in rows]
|
||
same = all(got) and len({g for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||
if not same:
|
||
for ln, g in zip(rows, got):
|
||
self.machine_line(ln, g)
|
||
if total:
|
||
self.subtotal(total, [call for _, call, _ in self.done[-len(rows) :]])
|
||
return
|
||
qty, place = got[0]
|
||
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
|
||
r = self.res(rows[0], None, None)
|
||
r |= {"단가": prices}
|
||
head = next(
|
||
(b for b in reversed(self.lines[: self.lines.index(rows[0])])
|
||
if (b.get("자원") or {}).get("코드") == r.get("코드") and not b.get("비목")),
|
||
{},
|
||
) # fmt: skip
|
||
title = head.get("품명") or r.get("원이름") or r.get("이름") or "줄"
|
||
name = self.o.names.item(title)
|
||
one = {"이름": name, "규격": r.get("규격") or "", "단위": r.get("단위") or "", "수량": qty}
|
||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": self.logic_link(r), "자원": r}
|
||
one |= {"자리": place} if place else {}
|
||
value = total["결과"][-1] if total and total.get("결과") else None
|
||
for ln in rows: # 노 · 재 · 경 글 = 펼침(맞댈 값은 한 줄 계 — 소계 글 · 없으면 첫 줄)
|
||
self.o.glyph(ln["행"], ln.get("보임") or "")
|
||
at = total or rows[0]
|
||
value = value if value is not None else sum(ln["결과"][-1] for ln in rows)
|
||
self.o.add_line(one, True, at["행"], at.get("보임") or "", value)
|
||
if total:
|
||
self.register(total, f"줄.'{name}'")
|
||
self.got["확정"] += 3
|
||
|
||
def machine_line(self, ln: dict, got: tuple | None) -> None:
|
||
qty, place = got if got else ("1", self.frozen(ln, "수량 식 못 읽음")[1])
|
||
price = (ln.get("연결단가") or [ln["결과"][-1]])[0]
|
||
r = self.res(ln, ln["비목"], price if got else ln["결과"][-1])
|
||
r["갈래"] = "기계" if r["갈래"] == "기계" else r["갈래"]
|
||
name = self.o.names.item(f"{ln.get('품명') or r.get('이름') or '줄'} {COMP[ln['비목']]}")
|
||
link, _ = em._m01(r["갈래"], r) if self.keys else (None, None)
|
||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||
one |= {"자리": place} if place else {}
|
||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||
self.got["확정"] += bool(got)
|
||
self.register(ln, f"줄.'{name}'")
|
||
|
||
def logic_link(self, r: dict) -> str | None:
|
||
"""기계 = 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 그 밖 = 없음(산근 = 형제 SK 참조)."""
|
||
if not self.keys or r.get("갈래") != "기계":
|
||
return None
|
||
key, _ = em._m01("기계", r)
|
||
if not key:
|
||
return None
|
||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||
|
||
got = dim.hourly_link(key, "노무비")
|
||
if not got:
|
||
return None
|
||
args = "".join(f", {k}={v}" for k, v in got["입력"].items())
|
||
return f"로직({got['키']}{args})"
|
||
|
||
def subtotal(
|
||
self, ln: dict, kids: list[str], part: str | None = None, glyph: bool = True
|
||
) -> str:
|
||
"""합 밖 소계 덧줄 · glyph 거짓 = 글줄에 안 붙임(같은 줄 식이 붙음 · 61 「합 ÷ N = 결과」)."""
|
||
name = self.o.names.line(self.label(ln) or "소계")
|
||
one = {"이름": name, "식": " + ".join(kids) or "0", "비목": COMP.get(part or "", "경비")}
|
||
one |= {"합밖": True, "출처": SUB}
|
||
value = ln["결과"][-1] if ln.get("결과") else (ln.get("저장") or {}).get("T")
|
||
if glyph:
|
||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", value)
|
||
else:
|
||
self.o.adds.append(one)
|
||
self.done.append((value or 0.0, name, ln))
|
||
return name
|
||
|
||
def sub61(self, i: int, ln: dict) -> None:
|
||
"""소계(61) — 앞 줄 결과 합(단 > 0 머리까지) · 아래 줄 비목 0 이면 그 줄이 소계 비목으로 합에 듦."""
|
||
want = (ln.get("줄참조") or [None])[0]
|
||
part = ln.get("비목")
|
||
start, skip = 0, set()
|
||
for k in range(i - 1, -1, -1): # node `native-text` 61 — 머리 · -20 줄까지(그 줄 포함)
|
||
b = self.lines[k]
|
||
if b.get("색인") == -1:
|
||
skip.add(b["행"])
|
||
if (b.get("단") or 0) > 0 or (b.get("색인") or 0) > 0 or b.get("색인") == -20:
|
||
start = b["행"]
|
||
break
|
||
kids = [d for d in self.done if start <= d[2]["행"] < ln["행"] and d[2]["행"] not in skip]
|
||
ok = want is not None and kids and _close(round(sum(k[0] for k in kids), 1), want)
|
||
if ok and ln.get("결과"): # 「인부소계 : ‹합› ÷ N = 결과」 — 합(합 밖) 뒤 식 줄이 합에 듦
|
||
self.subtotal({**ln, "결과": [want]}, [k[1] for k in kids], part, glyph=False)
|
||
got = self.money(ln)
|
||
body, place = got if got else self.frozen(ln, "소계 식 못 읽음")
|
||
name = self.o.names.line("소계식")
|
||
one = {"이름": name, "식": body, "비목": COMP.get(part or "", "경비")}
|
||
one |= ({} if part else {"합밖": True}) | ({"자리": place} if place else {})
|
||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||
self.done.append((ln["결과"][-1], name, ln))
|
||
self.got["확정"] += bool(got)
|
||
return
|
||
if ok and part and all(k[2].get("비목") is None and self._is_line(k[1]) for k in kids):
|
||
self.subtotal(ln, [k[1] for k in kids], part)
|
||
for _, call, kid in kids: # 아래 줄 = 소계 비목으로 합에 듦
|
||
self.joins(call, part)
|
||
self.got["확정"] += 1
|
||
return
|
||
if ok: # 그대로 — 소계 줄이 합에 듦
|
||
name = self.o.names.line("소계")
|
||
one = {
|
||
"이름": name,
|
||
"식": " + ".join(k[1] for k in kids),
|
||
"비목": COMP.get(part, "경비"),
|
||
}
|
||
one |= {} if part else {"합밖": True}
|
||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", want)
|
||
self.done.append((want, name, ln))
|
||
self.got["확정"] += 1
|
||
return
|
||
value = want if want is not None else 0.0
|
||
name = self.o.names.line("소계")
|
||
one = {"이름": name, "식": em.num(value), "비목": COMP.get(part, "경비")}
|
||
one |= {} if part else {"합밖": True}
|
||
self.mark(ln, "소계 아래 줄 못 가림 — 보인 값 얼림")
|
||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", value)
|
||
self.done.append((value, name, ln))
|
||
|
||
@staticmethod
|
||
def label(ln: dict) -> str:
|
||
"""「인부소계 : …」 → 「인부소계」(= 앞 쌍점만 · 낱말 아니면 빈 글)."""
|
||
head = _clean(ln.get("글") or "").split("=", 1)[0]
|
||
if ":" not in head:
|
||
return ""
|
||
got = re.sub(r"[^\w가-힣]", "", head.split(":", 1)[0])[:20]
|
||
return "" if not got or got[0].isdigit() else got
|
||
|
||
def _is_line(self, call: str) -> bool:
|
||
name = call[3:-1] if call.startswith("줄.'") else call
|
||
return any(one["이름"] == name for one in [*self.o.items, *self.o.adds])
|
||
|
||
def joins(self, call: str, part: str) -> None:
|
||
"""합 밖 호표 · 덧줄(부름 글) → 그 비목으로 합에 듦."""
|
||
name = call[3:-1] if call.startswith("줄.'") else call
|
||
for one in [*self.o.items, *self.o.adds]:
|
||
if one["이름"] != name:
|
||
continue
|
||
one.pop("합밖", None)
|
||
if one.get("종류") in ("인력", "재료", "기계") and "단가" in one:
|
||
one["비목"] = COMP[part]
|
||
r = one.get("자원") or {}
|
||
if r.get("비목"):
|
||
r["옮김"] = part if part != r["비목"] else None
|
||
elif "비목" in one and not one.get("종류"):
|
||
one["비목"] = COMP[part]
|
||
|
||
def rate(self, ln: dict) -> None:
|
||
got = self.money(ln)
|
||
body, place = got if got else self.frozen(ln, "율 식 못 읽음")
|
||
part = ln.get("비목")
|
||
name = self.o.names.line(self.label(ln) or "율")
|
||
one = {"이름": name, "식": body, "비목": COMP.get(part or "", "경비")}
|
||
one |= {} if part in COMP else {"합밖": True}
|
||
one |= {"자리": place} if place else {}
|
||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||
self.got["확정"] += bool(got)
|
||
self.register(ln, name)
|
||
|
||
def plain(self, ln: dict) -> None:
|
||
"""연결 없는 결과 줄 — 비목 있으면 덧줄(식) · 없으면 글 + 값(줄참조 대상이면 중간)."""
|
||
got = self.money(ln)
|
||
part = ln.get("비목")
|
||
if part in COMP:
|
||
body, place = got if got else self.frozen(ln, "식 못 읽음")
|
||
name = self.o.names.line("줄")
|
||
one = {"이름": name, "식": body, "비목": COMP[part]}
|
||
one |= {"자리": place} if place else {}
|
||
self.o.add_line(one, False, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||
self.register(ln, name)
|
||
self.got["확정"] += bool(got)
|
||
return
|
||
eng = self.o.names.line("값")
|
||
body = got[0] if got else em.num(ln["결과"][-1])
|
||
if got and got[1]:
|
||
body = f"{got[1]['방법']}({body}, {got[1]['자리']})"
|
||
self.o.middle(eng, body, None, ln["행"], ln.get("보임") or "")
|
||
self.register(ln, eng)
|
||
|
||
def text_line(self, ln: dict) -> None:
|
||
"""글 줄 — 품셈 · 조건 이름만 있는 글(값 없음) = 「추정」 표지(연결 안 함 · 810-3 화면)."""
|
||
if _COND.search(ln["보임"]):
|
||
self.o.glyph(
|
||
ln["행"], ln["보임"], mark=f"{NOTE}{GUESS} — 품셈 조건 글(값 없음 · 연결 안 함)"
|
||
)
|
||
self.got["추정"] += 1
|
||
return
|
||
self.o.glyph(ln["행"], ln["보임"])
|
||
|
||
# ── 한 벌 ─────────────────────────────────────────────────────
|
||
def run(self) -> dict:
|
||
for k, ln in enumerate(self.lines): # 「노무비 : ‹단가›」(식 · 결과 없음) = 단가 × 1
|
||
text = _clean(ln.get("글") or "")
|
||
if ln.get("연결단가") and not ln.get("결과") and "=" not in text and ln.get("비목"):
|
||
price = ln["연결단가"][0]
|
||
self.lines[k] = {**ln, "결과": [price], "글": f"{ln['글']} = {em.num(price)}"}
|
||
lines, i = self.lines, 0
|
||
while i < len(lines):
|
||
ln = lines[i]
|
||
link, part = ln.get("연결"), ln.get("비목")
|
||
if ln.get("합계") and link != 62:
|
||
if (ln.get("보임") or "").strip():
|
||
self.o.glyph(ln["행"], ln["보임"])
|
||
i += 1
|
||
continue
|
||
if link in TRIPLE and part in ("L", "M", "S"):
|
||
rows = [ln]
|
||
code = (ln.get("자원") or {}).get("코드")
|
||
while (
|
||
i + len(rows) < len(lines)
|
||
and lines[i + len(rows)].get("연결") == link
|
||
and lines[i + len(rows)].get("비목") in ("L", "M", "S")
|
||
and (lines[i + len(rows)].get("자원") or {}).get("코드") == code
|
||
):
|
||
rows.append(lines[i + len(rows)])
|
||
nxt = lines[i + len(rows)] if i + len(rows) < len(lines) else None
|
||
total = nxt if nxt and nxt.get("연결") == link and nxt.get("비목") == "T" else None
|
||
if all(r.get("결과") for r in rows):
|
||
self.triple(rows, total)
|
||
else:
|
||
for r in rows:
|
||
self.mark(r, "결과 없는 세 줄")
|
||
i += len(rows) + bool(total)
|
||
continue
|
||
if link in (61, 51, 68):
|
||
self.sub61(i, ln)
|
||
elif link in (58, 62, 52) and ln.get("결과"):
|
||
self.rate(ln)
|
||
elif ln.get("결과") and (ln.get("자원") or {}).get("코드") and ln.get("연결단가"):
|
||
self.item(ln, part if part in COMP else None, part in COMP)
|
||
elif ln.get("결과") and link in TRIPLE and part == "T":
|
||
self.o.glyph(ln["행"], ln.get("보임") or "", value=ln["결과"][-1])
|
||
self.register(ln, em.num(ln["결과"][-1]))
|
||
elif not ln.get("자원") and self.assigns(i, ln):
|
||
pass
|
||
elif ln.get("결과"):
|
||
self.plain(ln)
|
||
elif (ln.get("보임") or "").strip():
|
||
self.text_line(ln)
|
||
i += 1
|
||
saved = self.blk.get("저장") or {}
|
||
check = {COMP[c]: saved.get(c) or 0 for c in COMP} | {"계": saved.get("T") or 0}
|
||
out = self.o.done("비목버림", None, check, "산근", self.file)
|
||
out["원본"] |= {"원파일": True, "확정": dict(self.got)}
|
||
return out
|
||
|
||
|
||
def block(blk: dict, no: int, keys: bool = True, file: str = "", names: dict | None = None) -> dict:
|
||
"""EST 산근 블록(`M02_San_Estx.read` 산근 한 개 · 번 = 파일 안 차례 1부터) → {로직, 원본}.
|
||
names = 같은 파일 산근 코드 → 번(안쪽 산근 「단산N」)."""
|
||
return Block(blk, no, keys, file, names or {}).run()
|
||
|
||
|
||
_POW = re.compile(r"(\([^()]*\)|" + est.NUMRE + r"|[A-Za-z][A-Za-z0-9]*)\s*\^\s*(\d)(?![\d.])")
|
||
|
||
|
||
def _power(seg: str) -> str:
|
||
"""「x^2」 · 「(a+b)^2」 → 곱 되풀이(EST 토큰에 ^ 없음 · 정수 지수만)."""
|
||
for _ in range(8):
|
||
new = _POW.sub(lambda m: "(" + "×".join([m.group(1)] * int(m.group(2))) + ")", seg)
|
||
if new == seg:
|
||
break
|
||
seg = new
|
||
return seg
|