- 소요량_건설품셈_토목1장(116표) · 토목2장(9표) · 토목6장(73표) — md 표 73 · 8 · 50 개 모두 + 글 속 계수 표 - 변환 스크립트 build_소요량_건설품셈_토목.py + _토목_읽기 · _토목_표 · _토목_손 · _토목_계수 - check_master 틀 · 본문 · 로직 0건 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
195 lines
6.5 KiB
Python
195 lines
6.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""건설품셈 토목 제1 · 2 · 6장 md 읽기 · 표 만들기 · 쓰기 도구 (build_소요량_건설품셈_토목.py 가 씀).
|
||
|
||
값은 md 에서 읽음(손으로 적지 않음). 표 모양 다섯 갈래: 인력·장비 편성표(crew) · 줄이 관경인 납작표(flat) ·
|
||
요율 · 글 표 · 손 지정.
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
from pathlib import Path
|
||
|
||
from _md표_읽기 import N, R, Raw, join, js, unesc
|
||
|
||
ROOT = Path(__file__).resolve().parents[3]
|
||
BASE = ROOT / "resources/knowledge/original/원가계산/건설공사_표준품셈/본문/02_토목부문"
|
||
DIV = "건설품셈 토목 "
|
||
NUM = r"\d[\d,]*(?:\.\d+)?"
|
||
UNITS = "인 hr 시간 ㎜ ㎡ ㎥ m % 대 회 식 일 본 개소 개 ㎏ kg ton 톤 m/일".split()
|
||
ROLE = re.compile(r"^(수량|시공량|소요일수)")
|
||
|
||
|
||
class Tab:
|
||
def __init__(self, ident, title, mok, base, rows, k):
|
||
self.ident, self.title, self.mok, self.base, self.rows, self.k = (
|
||
ident,
|
||
title,
|
||
mok,
|
||
base,
|
||
rows,
|
||
k,
|
||
)
|
||
self.notes: list[str] = []
|
||
self.pre = "" # 표 앞 글줄(별표 제목 등)
|
||
|
||
|
||
def cells(line):
|
||
return [c.strip() for c in line.strip().strip("|").split("|")]
|
||
|
||
|
||
def title_of(s):
|
||
return re.sub(r"\s*\((?:['‘’]\d\d[^)]*)\)\s*$", "", s).strip()
|
||
|
||
|
||
def read_chapter(chdir):
|
||
"""장 폴더의 항 · 표 · 주 를 차례로 읽음 → (표 목록, 항 본문 줄 사전)."""
|
||
tabs, text, order = [], {}, []
|
||
for p in sorted((next(BASE.glob(chdir + "*"))).glob("*.md")):
|
||
L = p.read_text(encoding="utf-8").split("\n")
|
||
at = [i for i, x in enumerate(L) if x.strip() == "---"]
|
||
L = L[at[1] + 1 :]
|
||
ident = title = mok = base = pre = ""
|
||
k = 0
|
||
in_note = False
|
||
i = 0
|
||
while i < len(L):
|
||
ln = L[i].rstrip()
|
||
m = re.match(r"^(#{2,4})\s+(.*)$", ln)
|
||
if m:
|
||
in_note = False
|
||
d = len(m.group(1))
|
||
if d == 3:
|
||
mm = re.match(r"^(\d+-\d+-\d+)\s+(.*)$", m.group(2))
|
||
ident, title, mok, k = mm.group(1), title_of(mm.group(2)), "", 0
|
||
order.append(ident)
|
||
text[ident] = []
|
||
elif d == 4:
|
||
mk = re.match(r"^(\d+)\.\s*(.*)$", m.group(2))
|
||
mok = title_of(mk.group(2)) if mk else title_of(m.group(2))
|
||
i += 1
|
||
continue
|
||
if not ident:
|
||
i += 1
|
||
continue
|
||
text[ident].append(ln)
|
||
if ln.startswith("|"):
|
||
j = i
|
||
rows = []
|
||
while j < len(L) and L[j].startswith("|"):
|
||
c = cells(L[j])
|
||
if not all(re.fullmatch(r":?-+:?", x) for x in c):
|
||
rows.append(c)
|
||
j += 1
|
||
b = base if re.fullmatch(r"\(.*당\)|\(m/일\)|\(.*\)", base) else ""
|
||
tabs.append(Tab(ident, title, mok, base[1:-1] if b else "", rows, k))
|
||
tabs[-1].pre = pre
|
||
k += 1
|
||
base = pre = ""
|
||
in_note = False
|
||
i = j
|
||
continue
|
||
s = ln.strip()
|
||
if s == "[주]":
|
||
in_note = True
|
||
elif in_note and s and not s.startswith(("<!--", "![")):
|
||
if re.match(r"^[①-⑳]", s):
|
||
(tabs[-1].notes if tabs and tabs[-1].ident == ident else []).append(unesc(s))
|
||
elif tabs and tabs[-1].ident == ident and tabs[-1].notes:
|
||
tabs[-1].notes[-1] += " " + unesc(s)
|
||
elif re.fullmatch(r"\(.*\)", s) and not s.startswith("(주"):
|
||
base = s
|
||
elif s.startswith("<별표>"):
|
||
pre = s
|
||
i += 1
|
||
return tabs, text, order
|
||
|
||
|
||
def unit_split(h):
|
||
"""머리 「배관공(수도)(인)」 → (이름, 단위)."""
|
||
h = join(h)
|
||
m = re.search(r"\(([^()]*)\)$", h)
|
||
if m and m.group(1) in UNITS:
|
||
return h[: m.start()].strip(), m.group(1)
|
||
return h, ""
|
||
|
||
|
||
def fill_right(row, start):
|
||
out, prev = [], ""
|
||
for c in row[start:]:
|
||
prev = c if c else prev
|
||
out.append(prev)
|
||
return out
|
||
|
||
|
||
def is_num(x):
|
||
return bool(re.fullmatch(NUM, x.strip()))
|
||
|
||
|
||
class Out:
|
||
def __init__(self):
|
||
self.tables, self.keys = [], set()
|
||
|
||
def add(
|
||
self, t, label, cond, cols, rows, *, src=None, base=None, group=None, rule=None, notes=None
|
||
):
|
||
ident = t.ident if t else src
|
||
key = f"{ident} {label}"
|
||
assert key not in self.keys, key
|
||
self.keys.add(key)
|
||
d = {
|
||
"열쇠": key,
|
||
"이름": label,
|
||
"기준": (t.base if t else "") if base is None else base,
|
||
"출처": DIV + (src or ident),
|
||
}
|
||
if group:
|
||
d["그룹"] = group
|
||
d["조건"] = cond
|
||
if rule:
|
||
d["범위규칙"] = rule
|
||
d["값칸"] = cols
|
||
d["줄"] = rows
|
||
d["주"] = list(t.notes if (t and notes is None) else (notes or []))
|
||
self.tables.append(d)
|
||
|
||
def write(self, path):
|
||
L = ["{", ' "그룹": "소요량",', ' "원문": "건설품셈",', ' "판": "2026",', ' "표": [']
|
||
for ti, t in enumerate(self.tables):
|
||
L.append(" {")
|
||
keys = list(t)
|
||
for ki, k in enumerate(keys):
|
||
end = "," if ki < len(keys) - 1 else ""
|
||
v = t[k]
|
||
if k in ("줄", "주") and v:
|
||
L.append(f" {js(k)}: [")
|
||
L += [
|
||
f" {js(x)}" + ("," if xi < len(v) - 1 else "")
|
||
for xi, x in enumerate(v)
|
||
]
|
||
L.append(f" ]{end}")
|
||
else:
|
||
L.append(f" {js(k)}: {js(v)}{end}")
|
||
L.append(" }" + ("," if ti < len(self.tables) - 1 else ""))
|
||
L += [" ]", "}", ""]
|
||
Path(path).write_text("\n".join(L), encoding="utf-8", newline="\n")
|
||
|
||
|
||
def kind_of(vals):
|
||
"""조건 칸 종류 — 모두 수면 수 · 아니면 고르기."""
|
||
return "수" if all(is_num(v) for v in vals) else "고르기"
|
||
|
||
|
||
def nz(x):
|
||
return x not in ("", "-")
|
||
|
||
|
||
def cell_val(x):
|
||
"""값 칸 하나 → (값, 원문 or None). 수 하나가 든 글 = 수 + 원문 · 그 밖 글은 글."""
|
||
x = join(x)
|
||
if is_num(x):
|
||
return N(x), None
|
||
nums = re.findall(NUM, x)
|
||
if len(nums) == 1 and re.search(r"[가-힣%a-zA-Z]", x) and not re.search(r"[∼~\s]", x):
|
||
return R(nums[0]), x
|
||
return x, None
|