# -*- 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