- 소요량_건설품셈_토목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
323 lines
12 KiB
Python
323 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""토목 표 만들기 — 편성표(crew) · 납작표(flat) · 요율 · 매트릭스. (build_소요량_건설품셈_토목.py 가 씀)"""
|
||
|
||
import re
|
||
|
||
from _md표_읽기 import N, R, Raw, join
|
||
from _토목_읽기 import NUM, ROLE, UNITS, cell_val, fill_right, is_num, kind_of, nz, unit_split
|
||
|
||
|
||
def head_paths(rows, h, start, ncol):
|
||
"""머리 h 줄 → 열별 층 목록. 빈 칸은 같은 맨 윗 층 무리 안에서만 왼쪽 값으로 채움."""
|
||
top, paths = fill_right(rows[0][start:], 0), {c: [] for c in range(start, ncol)}
|
||
for r in range(h):
|
||
row = list(rows[r][start:])
|
||
for i in range(1, len(row)):
|
||
if not row[i] and r > 0 and top[i] == top[i - 1]:
|
||
row[i] = row[i - 1]
|
||
elif not row[i] and r == 0:
|
||
row[i] = row[i - 1]
|
||
for i, c in enumerate(range(start, ncol)):
|
||
v = jn(row[i]) if row[i] else ""
|
||
if v and (not paths[c] or paths[c][-1] != v):
|
||
paths[c].append(v)
|
||
return paths
|
||
|
||
|
||
def is_text(v):
|
||
return isinstance(v, str) and not isinstance(v, Raw)
|
||
|
||
|
||
def jn(cell):
|
||
"""칸 글 — 「∼」 둘레 줄바꿈은 붙임."""
|
||
if "∼" in cell:
|
||
return re.sub(r"\s*<br>\s*", "", cell).replace(" ∼", "∼").replace("∼ ", "∼")
|
||
return join(cell)
|
||
|
||
|
||
def is_unit(p):
|
||
return p in UNITS or bool(re.fullmatch(r"[㎡㎥㎜㎝㎏a-zA-Z/개소본일회]+", p))
|
||
|
||
|
||
def conds_out(names, rows):
|
||
"""조건 이름별 종류를 정해 줄의 글을 수로 바꿈 → 조건 사전."""
|
||
cond = {}
|
||
for nm in names:
|
||
vals = [r[nm] for r in rows if nm in r]
|
||
kd = kind_of(vals)
|
||
cond[nm] = kd
|
||
if kd == "수":
|
||
for r in rows:
|
||
if nm in r:
|
||
r[nm] = N(r[nm])
|
||
return cond
|
||
|
||
|
||
def bigo_items(text):
|
||
items = []
|
||
for p in (x.strip() for x in text.split("<br>")):
|
||
if p.startswith("- ") or not items:
|
||
items.append(p)
|
||
else:
|
||
items[-1] += " " + p
|
||
return ["비고 " + x for x in items]
|
||
|
||
|
||
def split_name(nm, spec):
|
||
"""이름 끝 괄호에 수가 들었으면(「…(폭 2.4m)」) 이름에서 떼어 규격 글로."""
|
||
m = re.search(r"\(([^()]*\d[^()]*)\)$", nm)
|
||
if m and " " not in nm[: m.start()].strip():
|
||
return nm[: m.start()].strip(), (m.group(1) + (" " + spec if spec else ""))
|
||
return nm, spec
|
||
|
||
|
||
# ── 편성표 ─────────────────────────────────────────────────────
|
||
def crew(t, cn=(), gname=None, prefix_group=False):
|
||
R0 = t.rows
|
||
head0 = R0[0]
|
||
uc = next(i for i, x in enumerate(head0) if join(x) == "단위")
|
||
sc = next((i for i, x in enumerate(head0) if join(x).startswith("규격")), None)
|
||
ncol = len(head0)
|
||
h = next(i for i in range(1, len(R0)) if R0[i][uc] != "")
|
||
namecols = [c for c in range(uc) if c != sc]
|
||
paths = head_paths(R0, h, uc + 1, ncol)
|
||
role, sig, cname_of = {}, {}, {}
|
||
for c, pth in paths.items():
|
||
ri = next((i for i, x in enumerate(pth) if ROLE.match(x)), None)
|
||
if ri is None: # 역할 글 없음(머리 = 조건) — 값은 모두 수량
|
||
role[c] = ("수량", "")
|
||
sig[c] = tuple(pth[1:] if len(pth) > 1 else pth)
|
||
cname_of[c] = (
|
||
[
|
||
re.sub(r"\(.*\)$", "", pth[0]).strip()
|
||
+ (
|
||
re.search(r"\(.*\)$", pth[0]).group(0)
|
||
if re.search(r"\(.*\)$", pth[0])
|
||
else ""
|
||
)
|
||
]
|
||
if len(pth) > 1
|
||
else []
|
||
)
|
||
continue
|
||
m = ROLE.match(pth[ri])
|
||
par = re.search(r"\((.*)\)", pth[ri])
|
||
p = par.group(1) if par else ""
|
||
role[c] = (m.group(1), p)
|
||
rest = [x for i, x in enumerate(pth) if i != ri]
|
||
sig[c] = tuple(rest)
|
||
cname_of[c] = [p] if (p and not is_unit(p) and rest) else []
|
||
prod = [c for c in role if role[c][0] != "수량"]
|
||
nc = namecols[-1]
|
||
for r in range(h, len(R0)): # 이름 칸이 비면(같은 이름 잇따름) 위 이름
|
||
if not R0[r][nc] and R0[r][uc] and R0[r - 1][nc]:
|
||
R0[r][nc] = R0[r - 1][nc]
|
||
# 블록
|
||
blocks, bigo, grp = [], [], ""
|
||
for r in range(h, len(R0)):
|
||
row = R0[r]
|
||
if join(row[0]) == "비고":
|
||
bigo += bigo_items(row[1])
|
||
continue
|
||
if len(namecols) > 1 and row[namecols[0]]:
|
||
grp = join(row[namecols[0]])
|
||
if (prod and any(row[c] for c in prod)) or not blocks:
|
||
blocks.append({"grp": grp, "rows": []})
|
||
blocks[-1]["rows"].append((grp, row))
|
||
cols, rows, condnames = {}, [], []
|
||
gcol = gname or (join(head0[namecols[0]]) if len(namecols) > 1 else "")
|
||
sigs = []
|
||
for c in sorted(role):
|
||
if sig[c] not in sigs:
|
||
sigs.append(sig[c])
|
||
nonempty = [s for s in sigs if s]
|
||
outsigs = nonempty if nonempty else [()]
|
||
for blk in blocks:
|
||
first = blk["rows"][0][1]
|
||
for sg in outsigs:
|
||
row = {}
|
||
if gcol and prod:
|
||
row[gcol] = blk["grp"]
|
||
names = list(cn) or next(
|
||
(cname_of[c] for c in role if sig[c] == sg and cname_of[c]), []
|
||
)
|
||
for lv, val in enumerate(sg):
|
||
nm = names[lv] if lv < len(names) else f"조건{lv + 1}"
|
||
if nm == "-":
|
||
continue
|
||
row[nm] = val[len(nm) + 1 :] if val.startswith(nm + " ") else val
|
||
vcols = [c for c in role if sig[c] in (sg, ())]
|
||
have_prod = False
|
||
for c in vcols:
|
||
if role[c][0] != "수량" and nz(first[c]):
|
||
nm = role[c][0]
|
||
pu = role[c][1] if is_unit(role[c][1]) else ""
|
||
cols[nm] = pu
|
||
row[nm] = N(first[c])
|
||
have_prod = True
|
||
if prod and not have_prod:
|
||
continue
|
||
for g, mrow in blk["rows"]:
|
||
nm = jn(mrow[namecols[-1]])
|
||
if prefix_group and g:
|
||
nm = f"{g} {nm}"
|
||
unit = mrow[uc]
|
||
for c in vcols:
|
||
if role[c][0] == "수량" and nz(mrow[c]):
|
||
assert is_num(mrow[c]), (t.ident, mrow[c])
|
||
spec = jn(mrow[sc]) if sc is not None and nz(mrow[sc]) else ""
|
||
nm, spec = split_name(nm, spec)
|
||
if nm in row: # 같은 이름 둘(규격만 다름) — 이름 뒤에 규격을 붙여 가름
|
||
nm = f"{nm} {spec}"
|
||
cols.setdefault(nm, unit)
|
||
row[nm] = N(mrow[c])
|
||
if spec and " " + spec not in nm:
|
||
cols[nm + "규격"] = "글"
|
||
row[nm + "규격"] = spec
|
||
rows.append(row)
|
||
cnames = []
|
||
for r in rows:
|
||
for k in r:
|
||
if k not in cnames and k not in cols:
|
||
cnames.append(k)
|
||
cond = conds_out(cnames, rows)
|
||
return cond, cols, rows, bigo
|
||
|
||
|
||
# ── 납작표 ─────────────────────────────────────────────────────
|
||
def flat(t, lead=1, levels=("V",), hdr=None, vname=None, halves=False, unit=None):
|
||
R0 = t.rows
|
||
bigo = []
|
||
body_rows = []
|
||
for row in R0:
|
||
if join(row[0]) == "비고":
|
||
bigo += bigo_items(row[1])
|
||
R0 = [r for r in R0 if join(r[0]) != "비고"]
|
||
if halves:
|
||
w = len(R0[0]) // 2
|
||
parts = [[r[:w] for r in R0], [r[w:] for r in R0]]
|
||
else:
|
||
parts = [R0]
|
||
cols, rows, condnames = {}, [], []
|
||
for P in parts:
|
||
h = hdr
|
||
if h is None:
|
||
h = next(i for i, row in enumerate(P) if any(is_num(c) for c in row[lead:]))
|
||
ncol = len(P[0])
|
||
lead_names = []
|
||
for c in range(lead):
|
||
hs = [jn(P[r][c]) for r in range(h) if P[r][c]]
|
||
lead_names.append(re.sub(r"\s+\(", "(", hs[-1]) if hs else f"조건{c + 1}")
|
||
paths = head_paths(P, h, lead, ncol)
|
||
info = {}
|
||
for c, pth in paths.items():
|
||
cn_, vn_ = {}, []
|
||
for i, el in enumerate(pth):
|
||
lv = levels[i] if i < len(levels) else "V"
|
||
if lv.startswith("C:"):
|
||
cn_[lv[2:]] = el
|
||
elif lv == "V":
|
||
vn_.append(el)
|
||
info[c] = (tuple(cn_.items()), " ".join(vn_))
|
||
prev = [""] * lead
|
||
for row in P[h:]:
|
||
if not any(row):
|
||
continue
|
||
lc = {}
|
||
for c in range(lead):
|
||
if row[c]:
|
||
prev[c] = jn(row[c])
|
||
for c2 in range(c + 1, lead):
|
||
prev[c2] = ""
|
||
lc[lead_names[c]] = prev[c]
|
||
sigs = []
|
||
for c in range(lead, ncol):
|
||
if info[c][0] not in sigs:
|
||
sigs.append(info[c][0])
|
||
for sg in sigs:
|
||
out = dict(lc)
|
||
for k, v in sg:
|
||
out[k] = v
|
||
got = False
|
||
for c in range(lead, ncol):
|
||
if info[c][0] != sg or not nz(row[c]):
|
||
continue
|
||
name, un = unit_split(info[c][1]) if info[c][1] else (vname, "")
|
||
if info[c][1] == "" or vname:
|
||
name = vname or name
|
||
un = unit or t.base
|
||
v, orig = cell_val(row[c])
|
||
cols[name] = "글" if is_text(v) or cols.get(name) == "글" else un
|
||
out[name] = v
|
||
if orig:
|
||
cols[name + "원문"] = "글"
|
||
out[name + "원문"] = orig
|
||
got = True
|
||
if got:
|
||
rows.append(out)
|
||
names = []
|
||
for r in rows:
|
||
for k in r:
|
||
if k not in names and k not in cols:
|
||
names.append(k)
|
||
cond = conds_out(names, rows)
|
||
return cond, cols, rows, bigo
|
||
|
||
|
||
def rate(t, cname=None, vname=None):
|
||
"""머리 = 조건, 둘째 줄 = 요율(%)."""
|
||
h, b = t.rows[0], t.rows[1]
|
||
cname = cname or jn(h[0])
|
||
vn = vname or re.sub(r"\(%\)$", "", jn(b[0]))
|
||
rows = [{cname: jn(h[c]), vn: R(b[c].rstrip("%"))} for c in range(1, len(h)) if nz(b[c])]
|
||
return {cname: "고르기"}, {vn: "%"}, rows, []
|
||
|
||
|
||
def matrix(t, rname, cname, vname, unit, hdr):
|
||
"""줄 머리 · 칸 머리 → 값 하나."""
|
||
P = t.rows
|
||
colh = [jn(x) for x in P[0]]
|
||
rows = []
|
||
for row in P[hdr:]:
|
||
for c in range(1, len(row)):
|
||
if nz(row[c]):
|
||
rows.append({rname: jn(row[0]), cname: colh[c], vname: N(row[c])})
|
||
cond = conds_out([rname, cname], rows)
|
||
return cond, {vname: unit}, rows, []
|
||
|
||
|
||
def tpose(t, cname, vname):
|
||
"""머리 = 조건, 둘째 줄 = 값(수 하나가 든 글은 수 + 원문)."""
|
||
h, b = t.rows[0], t.rows[1]
|
||
rows, cols = [], {}
|
||
for c in range(1, len(h)):
|
||
if not nz(b[c]):
|
||
continue
|
||
v, orig = cell_val(b[c])
|
||
d = {cname: jn(h[c]), vname: v}
|
||
cols[vname] = "글" if is_text(v) else ""
|
||
if orig:
|
||
d[vname + "원문"] = orig
|
||
cols[vname + "원문"] = "글"
|
||
rows.append(d)
|
||
return {cname: kind_of([r[cname] for r in rows])}, cols, rows, []
|
||
|
||
|
||
def members(rows, uc, sc, qc, nc=0):
|
||
"""몸 줄들 → (값칸, 값) — 수량 칸이 「-」 · 빈칸이면 뺌."""
|
||
cols, vals = {}, {}
|
||
for row in rows:
|
||
if not nz(row[qc]):
|
||
continue
|
||
nm = jn(row[nc])
|
||
spec = jn(row[sc]) if sc is not None and nz(row[sc]) else ""
|
||
nm, spec = split_name(nm, spec)
|
||
if nm in vals:
|
||
nm = f"{nm} {spec}"
|
||
cols.setdefault(nm, row[uc])
|
||
vals[nm] = N(row[qc])
|
||
if spec and " " + spec not in nm:
|
||
cols[nm + "규격"] = "글"
|
||
vals[nm + "규격"] = spec
|
||
return cols, vals
|