"""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_Doc_NativeLayout as lay 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"(? 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} self.memo: list[int] = [] # 글 줄 행(812-3 차림 · 분수 그림 위아래 줄 뺌) self.frac: set[int] = set() # ── 작은 도구 ────────────────────────────────────────────────── 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"(? 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] self.frac |= {self.lines[k]["행"] for k in (i - 1, i + 1) if 0 <= k < len(self.lines)} 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({repr(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 화면).""" self.memo.append(ln["행"]) if _COND.search(ln["보임"]): self.o.glyph( ln["행"], ln["보임"], mark=f"{NOTE}{GUESS} — 품셈 조건 글(값 없음 · 연결 안 함)" ) self.got["추정"] += 1 return self.o.glyph(ln["행"], ln["보임"]) def tags(self) -> None: """812-3 차림 표지(`M02_Doc_NativeLayout`) — 글줄 `차림` = 머리(단 > 0 · 소계 구간 첫 글 · 번호 글) · 합계 · 글(근거 · 메모).""" self.o.head[lay.TAG] = True for ln in self.lines: one = self.o.at.get(ln["행"]) if one is None or ln["행"] in self.frac or "자리" in one: continue head = (ln.get("단") or 0) > 0 or ln.get("색인") == -20 if ln.get("합계"): one[lay.TAG] = "합계" elif head or (ln["행"] in self.memo and lay.NUMBERED.match(one["글"])): one[lay.TAG] = "머리" elif ln["행"] in self.memo: one[lay.TAG] = "글" # ── 한 벌 ───────────────────────────────────────────────────── 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 self.tags() 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