- Basis 앞 몫(_ours · ours · clause · road_map · _grams · _sections · _named · _part · resolve · settle · has_logic · _has · no_basis) Clause 로 옮김 · Basis = 다시 내보내는 얇은 껍데기(순위 몫 candidates · merge · pick 은 785-7 까지 남김) - Excel: settle 부름 Clause 로 · ReadEst 별칭 hopyo_basis → hopyo_est - 새 지킴 시험 test_642_1_clause_formula(설계 642-A 5-3 첫 줄 기대 — 단산 10 · 46 · 24 · 47 · 39 후보 · 33 준용 · 6 옛 · 3 번호 없음 · 절 나무 · 상수 · 대입식 · 절 몫) - test_628: hb = Clause(정본) · 여덟 파일 표지 수 「기초 없음」 18 → 3 · 「옛 번호」 121 → 120 — 782-1 · 782-2 · 782-3 새 M01 기초로 낡은 수(옛 Basis 로도 같은 실패 확인) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.0 KiB
Python
164 lines
6.0 KiB
Python
"""785-1(642-1) 지킴 — 출처 풀이 · 조항 색인(`M02_Hopyo_Clause`) · 블록 글 → 절 나무 · 상수 ·
|
||
대입식 · 절 몫(`M02_Hopyo_Formula`) · 북부청 견본 엑셀(설계 642-A 5-3 첫 줄 기대).
|
||
읽기만 · 서버 없음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import warnings
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Basis as basis # noqa: E402
|
||
from M02_MasterTemplete import M02_Hopyo_Clause as clause # noqa: E402
|
||
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
|
||
from M02_MasterTemplete import M02_Hopyo_Formula as fm # noqa: E402
|
||
|
||
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
|
||
XLS = (
|
||
ROOT
|
||
/ "resources/knowledge/original/실무문서/2026_실무일위대가 모음"
|
||
/ "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls"
|
||
)
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def blocks() -> dict[int, dict]:
|
||
if not XLS.exists():
|
||
pytest.skip("견본 엑셀 없음")
|
||
book = hx.read_book(XLS.name, XLS.read_bytes())
|
||
return {b["번"]: b for b in book["단산"]}
|
||
|
||
|
||
@lru_cache(maxsize=None)
|
||
def got(no: int) -> dict:
|
||
return clause.basis(blocks()[no])
|
||
|
||
|
||
def keys(no: int) -> set[str]:
|
||
return {o["키"] for o in got(no)["후보"]}
|
||
|
||
|
||
# ── 조항 색인 → 후보 ──────────────────────────────────────────────────
|
||
@pytest.mark.parametrize(
|
||
"no, want",
|
||
[
|
||
(10, {"GC001140", "GC001141", "GC001142"}),
|
||
(46, {"GC000198"}),
|
||
(24, {"GC000211"}),
|
||
(47, {"GC000206", "GC000207"}),
|
||
(39, {"GF000224", "GF000225"}),
|
||
],
|
||
)
|
||
def test_후보(no, want):
|
||
assert keys(no) == want
|
||
assert got(no)["표지"] is None
|
||
|
||
|
||
def test_윗마디_절마다():
|
||
"""47 「6-2」 → 절 「1. 현장가공」 GC000206 · 「2. 조립」 GC000207 · 공장가공은 뺌."""
|
||
by = {o["키"]: (o["맞음"], o["절"]) for o in got(47)["후보"]}
|
||
assert by == {"GC000206": ("윗마디", "1"), "GC000207": ("윗마디", "2")}
|
||
assert {o["키"] for o in got(47)["뺌"]} >= {"GC000208"}
|
||
|
||
|
||
@pytest.mark.parametrize("no, mark", [(33, "준용"), (6, "옛 번호"), (3, "번호 없음")])
|
||
def test_표지(no, mark):
|
||
assert got(no)["표지"] == mark
|
||
|
||
|
||
def test_준용_결과단위_거름_절_번호():
|
||
"""33 「산림 12-12 준용」 → GF000177(원/식) 뺌 · 절 4 「표준품셈3-4-8」 → GC000110."""
|
||
r = got(33)
|
||
assert {o["키"]: o["까닭"][:7] for o in r["뺌"]} == {"GF000177": "결과단위 다름"}
|
||
assert [(o["키"], o["절"]) for o in r["후보"]] == [("GC000110", "4")]
|
||
|
||
|
||
def test_색인_옛이름():
|
||
assert set(clause.index()[("건설품셈 공통", "1-2-7")]) == {"GC001140", "GC001141", "GC001142"}
|
||
assert clause.nature("원/10㎡") == "면적" and clause.nature("원/자재 단위") is None
|
||
assert basis.settle is clause.settle and basis.no_basis is clause.no_basis
|
||
|
||
|
||
# ── 절 나무 ───────────────────────────────────────────────────────────
|
||
def _works(no: int) -> list[str]:
|
||
nodes = blocks()[no]["절"]
|
||
ids = {id(w) for w in fm.works(nodes)}
|
||
return [p for p, one in clause._paths(nodes) if id(one) in ids]
|
||
|
||
|
||
def test_절_나무():
|
||
assert _works(47) == ["1", "2"]
|
||
assert _works(6) == ["1.가", "1.나", "1.다", "3"]
|
||
assert [n["번"] for n in blocks()[6]["절"]] == ["1", "3", "4"] # 「2.」 빠짐 그대로
|
||
assert _works(24) == [] # 재료비 · 인건비 · 합계뿐 = 블록 통째
|
||
b = blocks()[47]
|
||
assert len(b["글행"]) == len(b["글"])
|
||
one = fm.find(b["절"], "1")
|
||
assert one["소계"] == {"노무비": 217540.1, "재료비": 19578.6, "경비": None, "계": 237118.7}
|
||
lo, hi = b["행"]
|
||
assert all(lo < r <= hi for r in one["행"])
|
||
|
||
|
||
# ── 상수 · 대입식 · 몫 ─────────────────────────────────────────────────
|
||
def _consts(no: int, path: str) -> dict[str, dict]:
|
||
return {c["기호"]: c for c in fm.find(blocks()[no]["절"], path)["상수"]}
|
||
|
||
|
||
def test_상수():
|
||
c = _consts(10, "1")
|
||
assert (c["L"]["값"], c["L"]["단위"]) == (6, "m")
|
||
assert (c["경사도"]["값"], c["경사도"]["단위"]) == (35, "°")
|
||
assert (c["n"]["값"], c["n"]["이름"]) == (42, "운반회수")
|
||
c = _consts(6, "1.나")
|
||
assert {k: c[k]["값"] for k in ("q", "K", "f", "E", "Cm")} == {
|
||
"q": 0.6,
|
||
"K": 0.55,
|
||
"f": 1,
|
||
"E": 0.35,
|
||
"Cm": 18,
|
||
}
|
||
assert (c["q"]["단위"], c["Cm"]["단위"], c["Cm"]["덧"]) == ("㎥", "sec", "90˚")
|
||
assert isinstance(c["q"]["행"], int)
|
||
|
||
|
||
def test_대입식():
|
||
p = next(f for f in fm.find(blocks()[10]["절"], "1")["식"] if f["기호"] == "P")
|
||
assert p["꼴"] == "곱" and p["값"] == 0.547619
|
||
assert [(f["수"], f["단위"]) for f in p["분자"]] == [
|
||
(1, "㎡"),
|
||
(575, "㎏/㎡"),
|
||
(1, "회"),
|
||
(1, "일"),
|
||
]
|
||
assert p["분모"] == [{"수": 25, "단위": "㎏"}, {"기호": "n", "단위": ""}]
|
||
assert p["대입"] == {"n": 42}
|
||
|
||
|
||
def test_절_몫():
|
||
nodes = blocks()[6]["절"]
|
||
share = fm.find(nodes, "1.나")["몫"]
|
||
assert share["글"] == "× 575 / 1750 × 90%"
|
||
assert share["값"] == pytest.approx(575 / 1750 * 0.9, abs=1e-6)
|
||
assert fm.find(nodes, "1.가")["몫"]["값"] == 0.1
|
||
assert fm.find(nodes, "3")["몫"]["값"] == pytest.approx(575 / 1750, abs=1e-6)
|
||
assert fm.find(blocks()[47]["절"], "1")["몫"] is None
|
||
|
||
|
||
def test_인자_괄호():
|
||
got_ = fm.factors("Cm × n / (60 × Es)")
|
||
assert [(w, f.get("기호") or f.get("수")) for w, f in got_] == [
|
||
("곱", "Cm"),
|
||
("곱", "n"),
|
||
("나눔", 60),
|
||
("나눔", "Es"),
|
||
]
|
||
assert fm.factors("t1 + t2") is None and fm.factors("1 2") is None
|