Files
eomsangdonandClaude Opus 5.5 428d35cbd6 feat(793-1): 경쟁사 산근 글 식 파서 · 참조 평가기 · 실무 여덟 파일 지킴 시험
- M02_San_Expr — eval 없는 식 나무 · 셈 · 줄 자리 규칙(버림 · 반올림 · 올림 × 0~3)
- M02_San_Stmate — 숨은 G 열 원문 → 줄 나무(계 5 단 · (-=) · 대입 · [A] · ;; · &L/M/S/X/D& · &P/R& 율 · :=A01 · -> · // 덧글)
- M02_San_Est — 보이는 글 → 식 복원(단가 · 앞줄 · 상수 역할 · 값 줄 숨은 합 · 변수 얼림 · 단가 정체 · 모호 표지)
- M02_San_Eval — STmate 셈(특수 % · 끝전 두 갈래 · 결과금액 조정 · 계약단가) · EST 다시 셈 · 줄마다 검산 · 단가 전파
- M02_San_Book — 같은 책 블록 · 단가표 읽기(읽기만)
- test_793_1 — STmate 233/233 · 금액 줄 1,315 · 계 줄 695 · EST 222/222 · 줄 2,901/2,903 · 모호 9 · 전파 견본 7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:59:56 +09:00

192 lines
7.2 KiB
Python
Raw Permalink 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.
"""STmate 산근 글 식 파서(PLAN 793-1 · 요구서_652 · 문법 = `_분석_648-B` 3 장 · eval 없음).
단가산출근거 시트 숨은 G 열 원문 한 줄 → 줄 나무(dict). 셈은 `M02_San_Eval.run_stmate`.
줄 꼴(`종류`):
- 빈 · 글 — 셈 없음(`'글'` · 단위 조각 · `// 덧글` 뺀 뒤 빈 것)
- 계 — `(=)`~`(=====)` 단 1~5 · `(-=)` 제외(위 합에서 뺌) · `(=):T01` 누계번지
- 대입 — `X = 식` 전 자리 · `X = 식 =` 소수 2 자리 반올림 · 한 줄 여러 대입(`,`) ·
맨 식(끼움 `[A]` 만 먹음)
- 참조 — `&L..&` · `&M..&` · `&S..&` · `&X..L/M/S(:L/M/S)&` · `&D..&` · `&B..&` 단가 × 계수 식
(특수 % 코드 L% · M% · T% 는 목록표 UNT 로 셀 때 가림)
- 율 — `&P{A01|A0102..|T01|C_|S_}{L|M|S|}:{L|M|S|}& × n'%'` 범위 금액 × n % · `&R..&` × n
- 공통 칸 — 숨김(`;;`) · 번지(`:=A01` · `:=T01`) · 빼기(`->` = 번지엔 쌓되 합에서 뺌) ·
찍기(끝 `=` 또는 번지) · 식(계수 나무 · `("참조",)` = 단가 자리 1) · 오류(읽기 실패 까닭)
"""
from __future__ import annotations
import re
from typing import Any
from M02_MasterTemplete import M02_San_Expr as ex
REF = re.compile(r"&([A-Z]+)([^&]*)&")
_TOK = re.compile(r"\s*(?:(\d+\.?\d*|\.\d+)|([A-Za-z][A-Za-z0-9_]*)|(@R)|(.))")
_TOTAL = re.compile(r"^\((-?)(=+)\)(?::(T\d+))?")
_NAME_EQ = re.compile(r"^\s*[A-Za-z][A-Za-z0-9_]*\s*=(?!=)")
_ASSIGN = re.compile(r"^([A-Za-z][A-Za-z0-9_]*)\s*=(?!=)(.*)$")
_ADDR = re.compile(r":=\s*([AT]\d+)\s*$")
_RATE_TAIL = re.compile(r"^(\d*|_)([LMS]?)(?::([LMS]?))?$")
_CODE_TAIL = re.compile(r"^([LMS]?)(?::([LMS]))?$")
RATE_KINDS = ("PA", "PT", "PC", "PS", "RA", "RT", "RC", "RS")
OPS = "+-*/^"
def tokens(text: str) -> list[tuple]:
"""STmate 식 글 → 토큰(끼움 `[..]` 안은 다시 토큰) · 모르는 기호 = ExprError."""
out: list[tuple] = []
at = 0
while at < len(text):
if text[at].isspace():
at += 1
continue
if text[at] == "[":
end = text.find("]", at)
if end < 0:
raise ex.ExprError("끼움 괄호 짝 없음")
inner = text[at + 1 : end]
if "=" in inner:
name, body = inner.split("=", 1)
out.append(("끼움", name.strip(), tokens(body)))
else:
out.append(("끼움", None, tokens(inner)))
at = end + 1
continue
m = _TOK.match(text, at)
if not m or m.end() == at:
break
at = m.end()
num, name, ref, other = m.groups()
if num:
out.append(("수", float(num)))
elif name:
out.append(("이름", name))
elif ref:
out.append(("참조",))
elif other and other.strip():
if other in OPS:
out.append(("연산", other))
elif other in "(),":
out.append((other,))
else:
raise ex.ExprError(f"모르는 기호 {other!r}")
return out
def expr(text: str) -> tuple:
"""식 글 → 나무 · 실패 = `("오류", 까닭)`(셀 때 터짐 — 측정 도구와 같은 차례)."""
try:
return ex.parse(tokens(text))
except ex.ExprError as error:
return ("오류", str(error))
def split_top(text: str, sep: str = ",") -> list[str]:
"""괄호 밖 `sep` 로 나눔."""
out, depth, cur = [], 0, ""
for ch in text:
if ch in "([":
depth += 1
elif ch in ")]":
depth -= 1
if ch == sep and depth == 0:
out.append(cur)
cur = ""
else:
cur += ch
out.append(cur)
return out
def _clean(g: str) -> tuple[str, bool, str | None]:
"""원문 → (셈 글, 숨김, 덧글) — `//` 뒤 덧글 · `;;` 숨은 줄 · `'글'` 지움."""
raw, _, note = g.partition("//")
hidden = raw.lstrip().startswith(";;")
s = raw.lstrip()[2:] if hidden else raw
s = re.sub(r"'[^']*'", " ", s).replace("'", " ")
return s.strip(), hidden, (note.strip() or None) if _ else None
def _ref_line(st: str, m: re.Match) -> dict[str, Any]:
kind, tail = m.group(1), m.group(2)
pre = _NAME_EQ.sub(" ", st[: m.start()])
body = pre + " @R " + st[m.end() :]
out: dict[str, Any] = {"빼기": "->" in body}
body = body.replace("->", " ")
addr = _ADDR.search(body)
out["번지"] = addr.group(1) if addr else None
if addr:
body = body[: addr.start()]
out["찍기"] = body.rstrip().endswith("=") or bool(addr)
out["식"] = expr(body.rstrip().rstrip("="))
if kind in RATE_KINDS:
mt = _RATE_TAIL.match(tail)
if not mt:
return {**out, "종류": "율", "오류": f"율 꼬리 {tail}"}
num = mt.group(1)
area = kind[1]
if area == "A":
spots = ["A" + num[i : i + 2] for i in range(0, len(num), 2)]
elif area == "T":
spots = ["T" + num]
else:
spots = []
rate = {"곱": kind[0] == "R", "범위": area, "번지": spots}
rate |= {"비목": mt.group(2) or None, "옮김": mt.group(3) or None}
return {**out, "종류": "율", "율": rate}
digits = re.match(r"^\d*", tail).group(0)
mc = _CODE_TAIL.match(tail[len(digits) :])
if not mc:
return {**out, "종류": "참조", "오류": f"참조 꼬리 {tail[len(digits) :]}"}
src = {"코드": kind + digits, "비목": mc.group(1) or None, "옮김": mc.group(2)}
return {**out, "종류": "참조", "자원": src}
def _assign_line(st: str) -> dict[str, Any]:
parts: list[dict[str, Any]] = []
for part in split_top(st):
p = part.strip()
if not p:
continue
ma = _ASSIGN.match(p)
if ma:
body = ma.group(2).strip()
show = body.endswith("=")
body = body.rstrip("=").strip()
if body:
parts.append({"이름": ma.group(1).lower(), "식": expr(body), "찍기": show})
else:
node = expr(p.rstrip("=").strip())
if node[0] not in ("오류", "이름"): # 맨 낱말 · 못 읽는 글 = 셈 없음
parts.append({"이름": None, "식": node, "찍기": False})
if not parts:
return {"종류": "글"}
return {"종류": "대입", "대입": parts}
def parse_line(g: str | None) -> dict[str, Any]:
"""G 열 원문 한 줄 → 줄 나무."""
st, hidden, note = _clean(g or "")
head = {"숨김": hidden, "덧글": note}
tot = _TOTAL.match(st.replace(" ", ""))
if tot:
return {**head, "종류": "계", "단": len(tot.group(2)), "제외": tot.group(1) == "-"} | {
"누계번지": tot.group(3)
}
if not st:
return {**head, "종류": "빈"}
refs = list(REF.finditer(st))
if len(refs) > 1:
return {**head, "종류": "참조", "오류": "참조 둘 이상", "식": ("오류", "참조 둘 이상")}
if refs:
return {**head, **_ref_line(st, refs[0])}
return {**head, **_assign_line(st)}
def parse_block(lines: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""[{행, G}] → 줄 나무 목록(행 · 원문 붙임)."""
return [
{"행": ln.get("행"), "글": ln.get("G") or "", **parse_line(ln.get("G"))} for ln in lines
]