# -*- 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*
\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("
")):
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