Merge remote-tracking branch 'origin/dev' into sub_laptop_2
- _키대장 「다음」 겹침 — 양쪽 큰 값(GF 337 · GC 1195 · GX 5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""로직 줄 칸 「개정」 — 원문 절 머리 개정 연도 목록 자동 채움(PLAN 794-2b · 분석 652-794-2a 5 장).
|
||||
|
||||
건설 = 본문 md 절 머리 괄호 「('08, '20, 25년 보완)」 · 「('19년 신설, '24년 보완)」 의 연도 전부.
|
||||
산림 = [2026](2026-01-01 제정 · 절 머리에 연도 없음). 절 머리에 연도가 없으면 윗절로 올라감.
|
||||
못 찾음 = 칸 없음(빈칸). 글 꼴 그대로 — 줄 「출처」 바로 뒤에 끼우거나 옛 값만 갈음.
|
||||
사람 입력 없음 · 셈 값 바뀜 0.
|
||||
|
||||
./venv/Scripts/python.exe resources/master_data/scripts/build_로직_개정.py [--check]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MASTER = ROOT / "resources/master_data"
|
||||
SRC = ROOT / "resources/knowledge/original/원가계산"
|
||||
PART = {"01": "공통", "02": "토목", "03": "건축", "04": "기계설비", "05": "유지관리"}
|
||||
HEAD = re.compile(r"^#{2,4}\s*(\d+(?:-\d+)+)\.?\s+(.*)$")
|
||||
FOREST = [2026]
|
||||
|
||||
|
||||
def years(title: str) -> list[int]:
|
||||
"""절 제목 괄호 속 「년」 글의 두 자리 연도 → [2008, 2020, 2025]."""
|
||||
found = {
|
||||
int(y) + (2000 if int(y) < 50 else 1900)
|
||||
for g in re.findall(r"\(([^()]*년[^()]*)\)", title)
|
||||
for y in re.findall(r"(?<!\d)(\d\d)(?!\d)", g)
|
||||
}
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def heads() -> dict[tuple, list[int]]:
|
||||
"""(책, 부문, 절 번호) → 개정 연도 목록(머리는 있는데 연도 없음 = [])."""
|
||||
out: dict[tuple, list[int]] = {}
|
||||
for f in sorted((SRC / "건설공사_표준품셈/본문").rglob("*.md")):
|
||||
part = PART.get(f.relative_to(SRC / "건설공사_표준품셈/본문").parts[0][:2])
|
||||
for line in f.read_text(encoding="utf-8").splitlines():
|
||||
if m := HEAD.match(line):
|
||||
out.setdefault(("건설품셈", part, m.group(1)), years(m.group(2)))
|
||||
for f in sorted((SRC / "산림_표준품셈/본문").rglob("*.md")):
|
||||
for line in f.read_text(encoding="utf-8").splitlines():
|
||||
if m := HEAD.match(line):
|
||||
out.setdefault(("산림품셈", None, m.group(1)), FOREST)
|
||||
return out
|
||||
|
||||
|
||||
def revision(h: dict, row: dict, book: str) -> tuple[list[int] | None, str]:
|
||||
"""(연도 목록 | None, 까닭) — 절 머리 → 윗절 차례로 연도 든 첫 머리."""
|
||||
num = str(row.get("원문번호") or "")
|
||||
if not num:
|
||||
return None, "원문번호 없음"
|
||||
part = str(row.get("구분") or "").replace("건설품셈", "").strip() or None
|
||||
seen = False
|
||||
while num:
|
||||
got = h.get((book, part if book == "건설품셈" else None, num))
|
||||
if got:
|
||||
return got, "채움"
|
||||
seen |= got is not None
|
||||
num = num.rsplit("-", 1)[0] if "-" in num else ""
|
||||
return None, "머리에 연도 없음" if seen else "절 머리 못 찾음"
|
||||
|
||||
|
||||
def _spans(text: str) -> list[tuple[int, int]]:
|
||||
"""「줄」 배열 속 줄 하나하나의 글 자리."""
|
||||
m = re.search(r'"줄"\s*:\s*\[', text)
|
||||
dec, pos, out = json.JSONDecoder(), m.end(), []
|
||||
while True:
|
||||
while text[pos] in " \t\r\n,":
|
||||
pos += 1
|
||||
if text[pos] == "]":
|
||||
return out
|
||||
_, end = dec.raw_decode(text, pos)
|
||||
out.append((pos, end))
|
||||
pos = end
|
||||
|
||||
|
||||
def put(row_text: str, value: list[int] | None) -> str:
|
||||
"""줄 글에 「개정」 끼움 · 갈음 · 지움 — 출처 바로 뒤 · 출처와 같은 구분자(한 줄이면 「, 」)."""
|
||||
old = re.search(r'"개정": \[[^\]]*\](,\s*)?', row_text)
|
||||
if old:
|
||||
row_text = row_text[: old.start()] + row_text[old.end() :]
|
||||
if value is None:
|
||||
return row_text
|
||||
src = re.search(r'"출처": "(?:[^"\\]|\\.)*"(,\s*)', row_text)
|
||||
if not src:
|
||||
raise ValueError("줄 출처 없음")
|
||||
sep = src.group(1)
|
||||
return row_text[: src.end()] + f'"개정": {json.dumps(value)}' + sep + row_text[src.end() :]
|
||||
|
||||
|
||||
def build(check: bool = False) -> Counter:
|
||||
h, why = heads(), Counter()
|
||||
for path in sorted(MASTER.glob("로직_*.json")):
|
||||
raw = path.read_bytes().decode("utf-8")
|
||||
data = json.loads(raw)
|
||||
if data.get("원문") not in ("건설품셈", "산림품셈"):
|
||||
continue
|
||||
rows, spans = data["줄"], _spans(raw)
|
||||
assert len(rows) == len(spans), path.name
|
||||
parts, last = [], 0
|
||||
for row, (s, e) in zip(rows, spans):
|
||||
value, tag = revision(h, row, data["원문"])
|
||||
why[tag] += 1
|
||||
why[row["키"][:2] + (" 채움" if value else " 빈칸")] += 1
|
||||
parts += [raw[last:s], put(raw[s:e], value)]
|
||||
if value:
|
||||
row["개정"] = value
|
||||
else:
|
||||
row.pop("개정", None)
|
||||
last = e
|
||||
out = "".join(parts + [raw[last:]])
|
||||
assert json.loads(out) == data, path.name # 끼운 칸 말고 같음
|
||||
if out != raw:
|
||||
why["바뀐 파일"] += 1
|
||||
if not check:
|
||||
path.write_bytes(out.encode("utf-8"))
|
||||
return why
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
got = build("--check" in sys.argv)
|
||||
for k, v in sorted(got.items()):
|
||||
print(f"{k}: {v}")
|
||||
@@ -0,0 +1,86 @@
|
||||
"""잔토처리(인력) 단독 로직 한 줄(PLAN 794-2b · 분석 652-794-2a 2 장 6 번 · 요구 653).
|
||||
|
||||
근거 = 건설 공통 3-2-1 비고 「현장 내에서 소운반하여 깔고 고르는 잔토처리는 ㎥당 보통인부
|
||||
0.2인을 별도 계상」 · 호표 보통인부 = QC000172 「잔토처리 보통인부」(GC000083 옵션과 같은 표).
|
||||
이미 있으면 그대로 · 글 꼴 그대로 GC000083 줄 뒤에 끼움 · 키 = `_키대장` 다음 GC 번호.
|
||||
뒤에 `build_로직_개정.py` 를 돌림.
|
||||
|
||||
./venv/Scripts/python.exe resources/master_data/scripts/build_로직_잔토처리.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
MASTER = Path(__file__).resolve().parents[1]
|
||||
FILE = MASTER / "로직_건설품셈_03장_토공사.json"
|
||||
BOOK = MASTER / "_키대장.json"
|
||||
NAME = "잔토처리(인력)"
|
||||
LEDGER = f"공통 3-2-1 {NAME}"
|
||||
AFTER = "GC000083"
|
||||
SRC = "건설품셈 공통 3-2-1 비고"
|
||||
|
||||
|
||||
def _row(key: str, nl: str) -> str:
|
||||
q = "찾기(QC000172, 구분='잔토처리 보통인부').값"
|
||||
lines = [
|
||||
"{",
|
||||
f' "키": "{key}", "원문번호": "3-2-1", "구분": "건설품셈 공통", '
|
||||
'"상세구분": "03장 토공사",',
|
||||
f' "이름": "{NAME}",',
|
||||
' "결과단위": "원/㎥",',
|
||||
f' "출처": "{SRC}",',
|
||||
' "소유": "공용",',
|
||||
' "비고": "현장 내 소운반하여 깔고 고르는 잔토처리 — ㎥당 보통인부 0.2인 별도 계상",', # noqa: E501
|
||||
' "입력": [],',
|
||||
' "중간": [],',
|
||||
' "호표": [',
|
||||
" {",
|
||||
' "종류": "인력",',
|
||||
' "요소": "LB000002",',
|
||||
' "이름": "보통인부",',
|
||||
' "단위": "인",',
|
||||
f' "수량": "{q}",',
|
||||
' "비목": "노무비",',
|
||||
f' "출처": "{SRC}"',
|
||||
" }",
|
||||
" ],",
|
||||
' "덧줄": [],',
|
||||
' "끝수": null',
|
||||
"}",
|
||||
]
|
||||
return (nl + " ").join(lines)
|
||||
|
||||
|
||||
def build() -> str:
|
||||
text = FILE.read_bytes().decode("utf-8")
|
||||
book = BOOK.read_bytes().decode("utf-8")
|
||||
have = re.search(rf'"(GC\d{{6}})": \{{"원문번호": "{re.escape(LEDGER)}"', book)
|
||||
if have and f'"키": "{have.group(1)}"' in text:
|
||||
return have.group(1)
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
bnl = "\r\n" if "\r\n" in book else "\n"
|
||||
n = int(re.search(r'"GC": (\d+)', book).group(1))
|
||||
key = f"GC{n:06d}"
|
||||
entry = json.dumps({"원문번호": LEDGER, "파일": FILE.name}, ensure_ascii=False)
|
||||
prev = re.search(rf' "GC{n - 1:06d}": .*?,{bnl}', book)
|
||||
book = book.replace(f'"GC": {n}', f'"GC": {n + 1}', 1)
|
||||
at = book.index(prev.group(0)) + len(prev.group(0))
|
||||
book = book[:at] + f' "{key}": {entry},{bnl}' + book[at:]
|
||||
# 줄 끼움 — AFTER 줄 글 끝 뒤
|
||||
start = text.index(f'"키": "{AFTER}"')
|
||||
start = text.rindex("{", 0, start)
|
||||
_, end = json.JSONDecoder().raw_decode(text, start)
|
||||
out = text[:end] + "," + nl + " " + _row(key, nl) + text[end:]
|
||||
data = json.loads(out)
|
||||
assert [r["키"] for r in data["줄"]].count(key) == 1
|
||||
json.loads(book)
|
||||
FILE.write_bytes(out.encode("utf-8"))
|
||||
BOOK.write_bytes(book.encode("utf-8"))
|
||||
return key
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(build())
|
||||
Reference in New Issue
Block a user