feat(data): 원천 벌 추출기 — 품셈 md → pum_forest·pum_const 를 줄 번호에 안 기대고 다시 뽑음
브레인 일감(막힌 자리). 코덱스가 md 에 줄을 보태자 원천 벌의 옛 줄 번호가 밀려 산림 밑수가 238 → 105 로
떨어졌고, 머리글로 다시 맞추니 12장 같은 머리글 이웃 표를 집어 직접공사비가 움직였음. 추출기가 저장소에 없었음.
- `resources/data_cost_input_value/_build_pum.py` — md `|` 표를 칸 글자 그대로(2단 머리 안 펼침)
· 표 번호·절은 옛 벌에서 이어받음 — ① 글자 같은 표를 차례 지키며 맞댐(이웃으로 못 건너뜀)
② 맞은 표 사이 수가 같으면 같은 차례끼리(글자만 고쳐진 표) ③ 다르면 그 안 머리 같은 표끼리
· 새 표는 가장 큰 번호 다음 · 사라진 표는 번호 버리고 알림 · 줄 번호·sha256 은 새 md 에서
· 옛 md(177d8844)에 돌리면 산림 476 · 건설 2192 표가 원천 벌과 글자까지 같음(표 번호·절·줄·칸)
- 다시 뽑음 — 산림: 줄만 밀린 351 · 글자 바뀐 표 0 / 건설: 줄만 밀린 199 · 새 표 10(C2193~C2202 ·
코덱스가 뭉친 글을 표로 되살린 제8장 손료표 · 뒤 번호 안 밀림)
- ⭐ 금액 불변 — 다시 뽑고 공종 마스터를 다시 지어 원가계산서를 잼: 직접공사비 5,983,724 → 5,983,724 ·
총원가 9,203,637 · 합계 10,124,000 그대로(원가계산서 열세 칸 전부 같음 · `test_work_item_key_gate`) · 열쇠 새 발급 0
· 이 커밋만으로는 원문 맞대기 둘이 빨강(마스터 줄 번호가 옛 md 기준 — 받기 전부터 빨강) · 마스터 다시 지으면 초록
· ⚠ 마스터 다시 짓기는 데스크탑 서브 몫이라 커밋에 안 넣음 — 지으면 밑수 238 → 242 · 미확보 92 → 88
(코덱스가 되살린 밑수 줄: 5-19-1 ㎡ · 5-27 10㎡ · 5-28-1 100㎡ · 9-21 100㎡) · 지문 시험은 그때 갱신
- `test_build_pum.py` — 원천 벌이 md 와 같음 · 줄 37 보태도 번호·절 그대로 · 표가 끼어도 뒤 번호 안 밀림 ·
12-17-1 같은 머리글 표 가운데를 고쳐도 제 번호 · 빠진 표는 알림 · 2단 머리 안 펼침
- 원문 맞대기 알려진 목록을 마스터 다시 지은 뒤 모습에 맞춤 —
⚠ 5-26-1·2·3 코덱스 복원 「(인/992㎡당)」 ↔ 마스터 사람 판정 100㎡ 어긋남(사람 판단 필요) ·
5-28-2 「(일 당)」 은 작업조 표라 마스터가 일부러 비움
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""원천 벌 뽑기 — 품셈 md → `pum_forest_2026.json` · `pum_const_2026.json` (2026-09-18 브레인).
|
||||
|
||||
왜 — 원문 md 가 고쳐지면(줄 보탬 · 표 머리 복원) 원천 벌의 **줄 번호가 밀려** 공종 마스터가 남의 줄을
|
||||
읽었음(산림 밑수 238 → 105). 머리글로 다시 맞추니 12장에 같은 머리글이 많아 **이웃 표를 집었고**
|
||||
직접공사비가 조용히 움직였음. 원천 벌을 뽑던 추출기가 저장소에 없었음.
|
||||
|
||||
무엇을 하나
|
||||
- 표 = md 의 `|` 표(머리 줄 + `|---|` 줄 + 몸 줄). 칸 글자를 **그대로** 옮김 —
|
||||
⚠ 2단 머리를 펼치지 않음(윗머리 「시 간 당(10-7)」 에 밑수·절 번호가 들어 있음).
|
||||
- ⚠ **줄 번호로 옛 표를 찾지 않음.** 표 번호(`F0001`·`C0001`)와 절(`section`)은 옛 벌에서 이어받되,
|
||||
짝은 이렇게 지음 — ① 글자(머리·몸)가 같은 표를 **차례를 지키며** 맞댐(앞뒤가 뒤바뀌지 않으니
|
||||
머리글이 같은 이웃 표로 건너뛸 수 없음) ② 맞은 표 사이 빈 자리의 표 수가 같으면 **같은 차례끼리**
|
||||
(글자만 고쳐진 표) ③ 수가 다르면 그 안에서 머리가 같은 표끼리 차례대로.
|
||||
- 새 표 = 가장 큰 번호 다음 번호 · 절은 표 위로 올라가며 처음 만나는 `###` 제목이나
|
||||
「숫자-숫자 글」 줄(옛 벌의 절 잡는 법 — 산림 476표가 이 규칙과 같음). ⚠ 알림에 뜸 — 사람이 볼 것.
|
||||
- 사라진 표 = 번호를 버리고 알림에 적음(다른 표에 번호를 물려주지 않음).
|
||||
- 줄 번호·sha256 은 새 md 에서. 표가 한 칸도 안 바뀌면 파일을 그대로 둠(`generated_at` 도).
|
||||
|
||||
돌리기: `./venv/Scripts/python.exe resources/data_cost_input_value/_build_pum.py [--check]`
|
||||
`--check` — 쓰지 않고 달라질 것만 알림 · 달라지면 끝 코드 1.
|
||||
금액 불변은 `resources/tester/test_work_item_key_gate.py` 원가계산서 시험(직접공사비 5,983,724)이 잼 —
|
||||
다시 뽑은 뒤 공종 마스터를 다시 짓고 그 시험을 돌릴 것.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CATALOGS = {
|
||||
"pum_forest": ROOT / "resources/data_cost_input_value/pum_forest_2026.json",
|
||||
"pum_const": ROOT / "resources/data_cost_input_value/pum_const_2026.json",
|
||||
}
|
||||
PREFIX = {"pum_forest": "F", "pum_const": "C"}
|
||||
|
||||
_SEP = re.compile(r"\s*\|(\s*:?-+:?\s*\|)+\s*")
|
||||
#: 옛 벌의 절 줄 — `12-2 표면 마무리를 따른다.` 처럼 본문 인용도 절로 잡혀 있음(마스터 빌더가 바로잡음).
|
||||
_SECTION_LINE = re.compile(r"(\d+-\d+(?:-\d+)*\.?\s+\S.*)$")
|
||||
|
||||
|
||||
def _cells(line: str) -> list[str]:
|
||||
s = line.strip()
|
||||
s = s[1:-1] if s.endswith("|") else s[1:]
|
||||
return [c.strip() for c in s.split("|")]
|
||||
|
||||
|
||||
def md_tables(lines: list[str]) -> list[dict]:
|
||||
"""md `|` 표 목록 — `line` 은 머리 줄 번호(1부터)."""
|
||||
tables, i = [], 0
|
||||
while i < len(lines):
|
||||
if (
|
||||
lines[i].lstrip().startswith("|")
|
||||
and i + 1 < len(lines)
|
||||
and _SEP.fullmatch(lines[i + 1])
|
||||
):
|
||||
j = i + 2
|
||||
while j < len(lines) and lines[j].lstrip().startswith("|"):
|
||||
j += 1
|
||||
tables.append(
|
||||
{
|
||||
"line": i + 1,
|
||||
"headers": _cells(lines[i]),
|
||||
"rows": [_cells(x) for x in lines[i + 2 : j]],
|
||||
}
|
||||
)
|
||||
i = j
|
||||
else:
|
||||
i += 1
|
||||
return tables
|
||||
|
||||
|
||||
def section_above(lines: list[str], line: int) -> str | None:
|
||||
"""새 표의 절 — 위로 올라가며 처음 만나는 `###` 제목 또는 「숫자-숫자 글」 줄(표 줄은 건너뜀)."""
|
||||
for k in range(line - 2, -1, -1):
|
||||
s = lines[k]
|
||||
if s.startswith("### "):
|
||||
return s[4:].strip()
|
||||
if s.lstrip().startswith("|"):
|
||||
continue
|
||||
if m := _SECTION_LINE.search(s):
|
||||
return m.group(1).strip()
|
||||
return None
|
||||
|
||||
|
||||
def _sig(table: dict) -> str:
|
||||
return json.dumps([table["headers"], table["rows"]], ensure_ascii=False)
|
||||
|
||||
|
||||
def align(old: list[dict], new: list[dict]) -> list[int | None]:
|
||||
"""`new[j]` 의 짝 `old` 차례(없으면 None). 차례를 지킴 — 짝끼리 앞뒤가 뒤바뀌지 않음."""
|
||||
pair: list[int | None] = [None] * len(new)
|
||||
|
||||
def pair_blocks(a: list[str], b: list[str], i0: int, j0: int, inner: Callable | None) -> None:
|
||||
matcher = difflib.SequenceMatcher(None, a, b, autojunk=False)
|
||||
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
||||
if tag == "equal" or (tag == "replace" and i2 - i1 == j2 - j1):
|
||||
for k in range(j2 - j1):
|
||||
pair[j0 + j1 + k] = i0 + i1 + k
|
||||
elif tag == "replace" and inner:
|
||||
inner(i0 + i1, i0 + i2, j0 + j1, j0 + j2)
|
||||
|
||||
def by_headers(i1: int, i2: int, j1: int, j2: int) -> None:
|
||||
heads = lambda ts: [json.dumps(t["headers"], ensure_ascii=False) for t in ts] # noqa: E731
|
||||
matcher = difflib.SequenceMatcher(
|
||||
None, heads(old[i1:i2]), heads(new[j1:j2]), autojunk=False
|
||||
)
|
||||
for tag, a1, a2, b1, b2 in matcher.get_opcodes():
|
||||
if tag == "equal":
|
||||
for k in range(b2 - b1):
|
||||
pair[j1 + b1 + k] = i1 + a1 + k
|
||||
|
||||
pair_blocks([_sig(t) for t in old], [_sig(t) for t in new], 0, 0, by_headers)
|
||||
return pair
|
||||
|
||||
|
||||
def extract(old_doc: dict, read: Callable[[str], bytes], dataset: str) -> tuple[dict, dict]:
|
||||
"""옛 벌 + 원문 → (새 벌, 알림). `read(path)` 는 원문 파일 바이트."""
|
||||
old_tables = old_doc["variables"]["pum"]["tables"]
|
||||
per_file = "source_file" in old_tables[0]
|
||||
sources = [s for s in old_doc["sources"] if s.get("role") == "primary"]
|
||||
numbers = [int(t["table_id"][1:]) for t in old_tables]
|
||||
next_number = max(numbers) + 1
|
||||
report = {"same": 0, "moved": 0, "changed": [], "new": [], "removed": []}
|
||||
tables, new_sources = [], []
|
||||
for src in old_doc["sources"]:
|
||||
data = read(src["path"])
|
||||
new_sources.append({**src, "sha256": hashlib.sha256(data).hexdigest()})
|
||||
for src in sources:
|
||||
lines = read(src["path"]).decode("utf-8").splitlines()
|
||||
old = [t for t in old_tables if not per_file or t["source_file"] == src["path"]]
|
||||
new = md_tables(lines)
|
||||
for j, k in enumerate(align(old, new)):
|
||||
t = new[j]
|
||||
if k is None:
|
||||
table_id = f"{PREFIX[dataset]}{next_number:04d}"
|
||||
next_number += 1
|
||||
section = section_above(lines, t["line"])
|
||||
report["new"].append({"table_id": table_id, "line": t["line"], "section": section})
|
||||
else:
|
||||
table_id, section = old[k]["table_id"], old[k]["section"]
|
||||
if _sig(old[k]) != _sig(t):
|
||||
report["changed"].append({"table_id": table_id, "line": t["line"]})
|
||||
elif old[k]["line"] != t["line"]:
|
||||
report["moved"] += 1
|
||||
else:
|
||||
report["same"] += 1
|
||||
entry = {"table_id": table_id, "section": section, "line": t["line"], "headers": t["headers"], "rows": t["rows"]} # fmt: skip
|
||||
if per_file:
|
||||
entry["source_file"] = src["path"]
|
||||
tables.append(entry)
|
||||
kept = {old[k]["table_id"] for k in align(old, new) if k is not None}
|
||||
report["removed"] += [{"table_id": t["table_id"], "section": t["section"]} for t in old if t["table_id"] not in kept] # fmt: skip
|
||||
doc = {**old_doc, "sources": new_sources}
|
||||
doc["variables"] = {
|
||||
**old_doc["variables"],
|
||||
"pum": {**old_doc["variables"]["pum"], "tables": tables},
|
||||
}
|
||||
if doc != old_doc:
|
||||
doc["generated_at"] = datetime.now(timezone(timedelta(hours=9))).isoformat(
|
||||
timespec="seconds"
|
||||
)
|
||||
return doc, report
|
||||
|
||||
|
||||
def _read(path: str) -> bytes:
|
||||
return (ROOT / path).read_bytes()
|
||||
|
||||
|
||||
def main(check: bool) -> int:
|
||||
dirty = False
|
||||
for dataset, path in CATALOGS.items():
|
||||
old_doc = json.loads(path.read_text(encoding="utf-8"))
|
||||
doc, report = extract(old_doc, _read, dataset)
|
||||
changed = doc != old_doc
|
||||
dirty |= changed
|
||||
print(
|
||||
f"── {dataset}: 표 {len(doc['variables']['pum']['tables'])} · 그대로 {report['same']} · "
|
||||
f"줄만 밀림 {report['moved']} · 글자 바뀜 {len(report['changed'])} · 새 표 {len(report['new'])} · "
|
||||
f"사라진 표 {len(report['removed'])}" + ("" if changed else " — 바뀐 것 없음")
|
||||
)
|
||||
for key in ("changed", "new", "removed"):
|
||||
for item in report[key]:
|
||||
print(f" {key}: {json.dumps(item, ensure_ascii=False)}")
|
||||
if changed and not check:
|
||||
path.write_text(json.dumps(doc, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return 1 if (check and dirty) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main("--check" in sys.argv))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -130,30 +130,52 @@
|
||||
},
|
||||
"F0140": {
|
||||
"section": "5-26-1",
|
||||
"md": null,
|
||||
"md_evidence": "",
|
||||
"md": [
|
||||
992.0,
|
||||
"㎡"
|
||||
],
|
||||
"md_evidence": "(인/992㎡당)",
|
||||
"master": [
|
||||
100.0,
|
||||
"㎡"
|
||||
]
|
||||
],
|
||||
"why": "⚠ 코덱스 복원 「(인/992㎡당)」 ↔ 마스터 사람 판정 100㎡(Forms.py BASIS_JUDGMENTS · 머리 소실 표) — 둘 중 하나가 틀림, 사람 판단"
|
||||
},
|
||||
"F0141": {
|
||||
"section": "5-26-2",
|
||||
"md": null,
|
||||
"md_evidence": "",
|
||||
"md": [
|
||||
992.0,
|
||||
"㎡"
|
||||
],
|
||||
"md_evidence": "(인/992㎡당)",
|
||||
"master": [
|
||||
100.0,
|
||||
"㎡"
|
||||
]
|
||||
],
|
||||
"why": "위와 같음(5-26-2)"
|
||||
},
|
||||
"F0142": {
|
||||
"section": "5-26-3",
|
||||
"md": null,
|
||||
"md_evidence": "",
|
||||
"md": [
|
||||
992.0,
|
||||
"㎡"
|
||||
],
|
||||
"md_evidence": "(인/992㎡당)",
|
||||
"master": [
|
||||
100.0,
|
||||
"㎡"
|
||||
]
|
||||
],
|
||||
"why": "위와 같음(5-26-3)"
|
||||
},
|
||||
"F0145": {
|
||||
"section": "5-28-2",
|
||||
"md": [
|
||||
1.0,
|
||||
"일"
|
||||
],
|
||||
"md_evidence": "(일 당)",
|
||||
"master": null,
|
||||
"why": "작업조 표(시공량 열이 밑수) — 마스터가 일부러 비움(c3da333e 와 같은 까닭) · 원문 「(일 당)」 은 코덱스 복원(61f35108)"
|
||||
},
|
||||
"F0173": {
|
||||
"section": "7-2",
|
||||
@@ -227,24 +249,6 @@
|
||||
"본"
|
||||
]
|
||||
},
|
||||
"F0326": {
|
||||
"section": "11-2",
|
||||
"md": null,
|
||||
"md_evidence": "",
|
||||
"master": [
|
||||
1.0,
|
||||
"개소"
|
||||
]
|
||||
},
|
||||
"F0327": {
|
||||
"section": "11-3",
|
||||
"md": null,
|
||||
"md_evidence": "",
|
||||
"master": [
|
||||
1.0,
|
||||
"개소"
|
||||
]
|
||||
},
|
||||
"F0338": {
|
||||
"section": "12-6",
|
||||
"md": [
|
||||
@@ -319,6 +323,13 @@
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"F0083": {
|
||||
"section": "4-1-1",
|
||||
"md_only": [],
|
||||
"master_only": [
|
||||
"- 3. 박피품은 별도 적용하여야 한다. 다만 작업조건에 따라 1일 작업량은 변동 적용한다."
|
||||
]
|
||||
},
|
||||
"F0104": {
|
||||
"section": "5-3-4",
|
||||
"md_only": [],
|
||||
@@ -356,28 +367,6 @@
|
||||
"- ④ 할인․할증은 경사도(1-4-5), 주행 장애물 상태(1-4-13)를 반영한다."
|
||||
]
|
||||
},
|
||||
"F0323": {
|
||||
"section": "10-13-2",
|
||||
"md_only": [],
|
||||
"master_only": [
|
||||
"[주] 위 단가산출은 일본 임야청에서 묘목 5,000본의 운반을 가정하여 현장 검증을 통해 산출한 내용이므로, 묘목 이외의 다른 화물을 운반할 시에는 달라질 수 있다."
|
||||
]
|
||||
},
|
||||
"F0393": {
|
||||
"section": "12-38-2",
|
||||
"md_only": [],
|
||||
"master_only": [
|
||||
"[주] ① 재료량에는 재료의 할증 및 손율이 포함되어 있다.",
|
||||
"- ② 플랫 타이(Flat Tie) 대신 폼 타이(Form Tie) 사용 시 소요 수량은 12-4 합판 거푸집 자재수량을 따른다."
|
||||
]
|
||||
},
|
||||
"F0083": {
|
||||
"section": "4-1-1",
|
||||
"md_only": [],
|
||||
"master_only": [
|
||||
"- 3. 박피품은 별도 적용하여야 한다. 다만 작업조건에 따라 1일 작업량은 변동 적용한다."
|
||||
]
|
||||
},
|
||||
"F0223": {
|
||||
"section": "8-4",
|
||||
"md_only": [],
|
||||
@@ -470,6 +459,21 @@
|
||||
"master_only": [
|
||||
"- ③ 「건설공사 표준품셈 공통부문. 8-2-1 불도저(2025)」 참조한다."
|
||||
]
|
||||
},
|
||||
"F0323": {
|
||||
"section": "10-13-2",
|
||||
"md_only": [],
|
||||
"master_only": [
|
||||
"[주] 위 단가산출은 일본 임야청에서 묘목 5,000본의 운반을 가정하여 현장 검증을 통해 산출한 내용이므로, 묘목 이외의 다른 화물을 운반할 시에는 달라질 수 있다."
|
||||
]
|
||||
},
|
||||
"F0393": {
|
||||
"section": "12-38-2",
|
||||
"md_only": [],
|
||||
"master_only": [
|
||||
"[주] ① 재료량에는 재료의 할증 및 손율이 포함되어 있다.",
|
||||
"- ② 플랫 타이(Flat Tie) 대신 폼 타이(Form Tie) 사용 시 소요 수량은 12-4 합판 거푸집 자재수량을 따른다."
|
||||
]
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -573,6 +577,19 @@
|
||||
}
|
||||
},
|
||||
"cost_rule": {
|
||||
"F0369": {
|
||||
"section": "12-22",
|
||||
"master": "sub_requirement",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비"
|
||||
],
|
||||
[
|
||||
"설치비",
|
||||
"재료비의 5%"
|
||||
]
|
||||
]
|
||||
},
|
||||
"F0379": {
|
||||
"section": "12-29",
|
||||
"master": "requirement",
|
||||
@@ -589,19 +606,6 @@
|
||||
],
|
||||
"why": "⚠ 같은 꼴 6표는 reference 인데 이것만 requirement — 설치비 = 재료비의 5%"
|
||||
},
|
||||
"F0369": {
|
||||
"section": "12-22",
|
||||
"master": "sub_requirement",
|
||||
"md_rows": [
|
||||
[
|
||||
"재료비"
|
||||
],
|
||||
[
|
||||
"설치비",
|
||||
"재료비의 5%"
|
||||
]
|
||||
]
|
||||
},
|
||||
"F0382": {
|
||||
"section": "12-31",
|
||||
"master": "sub_requirement",
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""원천 벌 뽑기(`_build_pum.py`) 시험 — 2026-09-18 브레인 일감(랩탑 메인).
|
||||
|
||||
못박는 것
|
||||
- 원천 벌이 **지금 md 와 같음** — md 가 고쳐졌는데 안 뽑았으면 빨강(뽑기를 돌리라는 뜻).
|
||||
- ⚠ **줄이 밀려도 표 번호·절이 그대로** — 이번 막힘(밑수 238 → 105)의 원인 자리.
|
||||
- 표가 끼어들어도 **뒤 번호가 안 밀림** · 글자만 고쳐진 표는 **제 번호** · 사라진 표는 **알림**.
|
||||
- ⚠ 머리글이 같은 이웃 표(12-17-1 펌프카 타설 일곱 표)로 **건너뛰지 않음** — 서브가 겪은 사고.
|
||||
금액 불변 — 뽑은 뒤 공종 마스터를 다시 짓고 `test_work_item_key_gate.py` 원가계산서 시험
|
||||
(직접공사비 5,983,724)이 잼. 이 파일은 그 앞 단계(원천 벌 표 번호·절·칸 글자)가 안 흔들림을 잼.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "resources" / "data_cost_input_value"))
|
||||
|
||||
import _build_pum as build # noqa: E402
|
||||
|
||||
FOREST = build.CATALOGS["pum_forest"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def forest() -> dict:
|
||||
"""지금 md 에서 뽑은 산림 벌 — 아래 시험은 이것에 줄을 보태고·표를 끼우고·고치고·뺌."""
|
||||
committed = json.loads(FOREST.read_text(encoding="utf-8"))
|
||||
return build.extract(committed, build._read, "pum_forest")[0]
|
||||
|
||||
|
||||
def _lines_of(doc: dict) -> list[str]:
|
||||
return (ROOT / doc["sources"][0]["path"]).read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
def _reader(lines: list[str]):
|
||||
return lambda path: "\n".join(lines).encode("utf-8")
|
||||
|
||||
|
||||
def _tables(doc: dict) -> list[dict]:
|
||||
return doc["variables"]["pum"]["tables"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dataset", list(build.CATALOGS))
|
||||
def test_원천_벌이_지금_md_와_같음(dataset: str) -> None:
|
||||
"""md 가 고쳐졌으면 `_build_pum.py` 를 돌릴 것. sha 는 줄끝(CRLF) 따라 PC 마다 갈려 안 봄."""
|
||||
old = json.loads(build.CATALOGS[dataset].read_text(encoding="utf-8"))
|
||||
doc, report = build.extract(old, build._read, dataset)
|
||||
stale = {k: v for k, v in report.items() if k != "same" and v}
|
||||
assert _tables(doc) == _tables(old), (
|
||||
f"{dataset} 원천 벌이 md 와 다름 — _build_pum.py 돌릴 것: {stale}"
|
||||
)
|
||||
|
||||
|
||||
def test_줄이_밀려도_표_번호와_절이_그대로(forest: dict) -> None:
|
||||
lines = _lines_of(forest)
|
||||
at = next(i for i, x in enumerate(lines) if x.startswith("### 5-1."))
|
||||
shifted = lines[:at] + ["보탠 줄"] * 37 + lines[at:]
|
||||
doc, report = build.extract(forest, _reader(shifted), "pum_forest")
|
||||
before, after = _tables(forest), _tables(doc)
|
||||
assert [(t["table_id"], t["section"]) for t in after] == [(t["table_id"], t["section"]) for t in before] # fmt: skip
|
||||
assert [t["line"] - b["line"] for t, b in zip(after, before)] == [37 if b["line"] > at else 0 for b in before] # fmt: skip
|
||||
assert report["changed"] == report["new"] == report["removed"] == []
|
||||
|
||||
|
||||
def test_표가_끼어들어도_뒤_번호가_안_밀리고_새_번호를_받음(forest: dict) -> None:
|
||||
lines = _lines_of(forest)
|
||||
at = next(i for i, x in enumerate(lines) if x.startswith("### 9-2."))
|
||||
inserted = lines[: at + 1] + ["", "| 새 | 표 |", "|---|---|", "| 1 | 2 |", ""] + lines[at + 1 :]
|
||||
doc, report = build.extract(forest, _reader(inserted), "pum_forest")
|
||||
ids = [t["table_id"] for t in _tables(doc)]
|
||||
assert [t["table_id"] for t in _tables(forest)] == [i for i in ids if i != "F0477"]
|
||||
assert [(n["table_id"], n["section"]) for n in report["new"]] == [("F0477", "9-2. 노선 굴진 보조원")] # fmt: skip
|
||||
assert report["removed"] == [] and report["changed"] == []
|
||||
|
||||
|
||||
def test_글자만_고쳐진_표는_제_번호_머리글_같은_이웃으로_안_건너뜀(forest: dict) -> None:
|
||||
"""12-17-1 펌프카 타설 — 같은 머리글 표가 줄지어 있음. 가운데 표 한 칸을 고쳐도 번호가 제자리."""
|
||||
lines = _lines_of(forest)
|
||||
target = next(t for t in _tables(forest) if t["table_id"] == "F0359")
|
||||
row = target["line"] + 1 # 첫 몸 줄(0부터 셈: 머리 line-1 · 구분줄 line · 몸 line+1)
|
||||
edited = copy.copy(lines)
|
||||
edited[row] = edited[row].replace("|", "| 고침", 1) # 첫 칸 글자만 바꿈
|
||||
doc, report = build.extract(forest, _reader(edited), "pum_forest")
|
||||
assert [t["table_id"] for t in _tables(doc)] == [t["table_id"] for t in _tables(forest)]
|
||||
assert report["changed"] == [{"table_id": "F0359", "line": target["line"]}]
|
||||
|
||||
|
||||
def test_표가_빠지면_번호를_버리고_알림(forest: dict) -> None:
|
||||
lines = _lines_of(forest)
|
||||
target = next(t for t in _tables(forest) if t["table_id"] == "F0358")
|
||||
end = target["line"] + 1 + len(target["rows"]) # 머리·구분줄·몸
|
||||
removed = lines[: target["line"] - 1] + lines[end:]
|
||||
doc, report = build.extract(forest, _reader(removed), "pum_forest")
|
||||
ids = [t["table_id"] for t in _tables(doc)]
|
||||
assert "F0358" not in ids and ids == [t["table_id"] for t in _tables(forest) if t["table_id"] != "F0358"] # fmt: skip
|
||||
assert [r["table_id"] for r in report["removed"]] == ["F0358"] and report["new"] == []
|
||||
|
||||
|
||||
def test_2단_머리를_펼치지_않음() -> None:
|
||||
"""윗머리 줄은 표 머리(`headers`)로, 아랫머리는 첫 몸 줄로 **글자 그대로** 남음."""
|
||||
lines = ["### 8-3-1 손료", "", "| 분류 번호 | 시 간 당(10-7) | |", "|---|---|---|", "| | 상각비 계수 | 계 |", "| 0101-0007 | 1,811 | 2,000 |"] # fmt: skip
|
||||
(table,) = build.md_tables(lines)
|
||||
assert table["headers"] == ["분류 번호", "시 간 당(10-7)", ""]
|
||||
assert table["rows"][0] == ["", "상각비 계수", "계"]
|
||||
Reference in New Issue
Block a user