- 새 M02_San_Basis_Map — 옛 번호 대조표(건설 옛 11-3 → 8-2-3 · 3-7 → 3-9-1 · 8-2-35 → 8-2-32 · 근거 = 원문 표 값 · 실무 글) · 사방(옛) = 판 대응표 2018 통합 판 · 기계 상수 줄 글 읽기(여러 대입 한 줄 · 괄호 설명 · 끝값 없는 나눗셈) · 작업량당 기계경비 로직 낱말 표 - 엔진 — 같은 말(휠라 = 채움재 · 평떼 = 평잔디 · 육샹 = 육상) · 건설(옛) 같은 번호는 같은 절 + 제목 뜻 겹침만 · Q 식 절 기호 채움(고르는 차례는 채우기 전) · 값 · 제목 길이 품 검산으로 안 받쳐지면 그 기계 작업량당 기계경비(차례 「기계」 · 번호 길은 그대로) - 검산 probe — 고르기가 범위뿐이면 용량 · 규격 값이 드는 범위 하나로(버킷용량 → 규격구분) - 459 블록: 없음 85 → 58 · 확정 125 → 164 · 근접 135 → 145 · 사람 114 → 92 · 확정 · 근접 → 없음 0 · 섞기(GF000132) 오인 넷 → GC001148 · GC001189 - 시험 test_793_3a 넓힘 — 없음 ≤ 58 · 확정 ≥ 164 · 손 확인 견본 열 · 1/3 적용 지어 넣지 않음 · 상수 읽기 · 대조표 - 근거후보 꼴 그대로 · M01 · 지식DB 고침 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
260 lines
11 KiB
Python
260 lines
11 KiB
Python
"""793-3a 지킴 — 근거 제안 엔진(`M02_San_Basis*`) · 실무 여덟 파일 산근 459 블록.
|
||
|
||
판정 수 ≥ 분석 648-D2 합친 길(기초 짚음 260 · 옵션 전부 52 · 품 검산 맞음 141 · 없음 94) ·
|
||
손 확인 견본(합판거푸집 넷 · 철근 Type-Ⅱ 가공 + 조립 · 레미콘 소량감률 50 · 터파기 #51 ·
|
||
야면석소운반 GC001140 석재류 · 불량 · 큰돌쌓기 ÷10 근접) · 지어 넣은 값 0 · 보정 자료 = 여덟 파일 ·
|
||
저장본 길(임시 폴더). 엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만 · 없으면 skip) ·
|
||
STmate 숨은 G 식 = 올림 길과 같은 `M02_San_Basis_Store.hidden`.
|
||
794-2c 넓힘 — 없음 85 → 58 · 확정 단 125 → 164 · 손 확인 견본 열(기계 작업량 길 · 섞기 오인 넷 ·
|
||
옛 번호 대조 · 같은 말 묶음) · 기계 상수 읽기 · 옛 번호 대조표.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
from collections import Counter
|
||
from functools import lru_cache
|
||
|
||
import helper_m02_hopyo_golden as golden
|
||
import pytest
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Judge as judge
|
||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||
from M02_MasterTemplete import M02_San_Basis as sb
|
||
from M02_MasterTemplete import M02_San_Basis_Const as bc
|
||
from M02_MasterTemplete import M02_San_Basis_Index as bi
|
||
from M02_MasterTemplete import M02_San_Basis_Map as bm
|
||
from M02_MasterTemplete import M02_San_Basis_Store as store
|
||
from M02_MasterTemplete import M02_San_Book as san_book
|
||
|
||
WAYS = {
|
||
"B1",
|
||
"B2",
|
||
"B3",
|
||
"B4",
|
||
"B5",
|
||
"B6",
|
||
"V",
|
||
"검산",
|
||
"검산(같은 품 여럿)",
|
||
"근접",
|
||
"근접(같은 품 여럿)",
|
||
}
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def books() -> list[tuple]:
|
||
"""[(파일, HP 꼴 책 + 숨은식)] — 없는 파일이면 skip."""
|
||
out = []
|
||
for tag, path in golden.FILES.items():
|
||
book = golden.read(tag) if path.exists() else None
|
||
if book is None:
|
||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||
sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {}
|
||
out.append((tag, {**book, "숨은식": store.hidden(book, sheets)}))
|
||
return out
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def results() -> tuple[dict, float]:
|
||
sb.ctx()
|
||
t0 = time.time()
|
||
got = {}
|
||
for tag, book in books():
|
||
for blk in book["단산"]:
|
||
got[f"{tag}#{blk['번']}"] = store.one(book, int(blk["번"]))
|
||
return got, time.time() - t0
|
||
|
||
|
||
def test_counts_at_least_d2():
|
||
got, sec = results()
|
||
kind = Counter(r["꼴"] for r in got.values())
|
||
tier = Counter(r["단"] for r in got.values())
|
||
same = sum(
|
||
1
|
||
for r in got.values()
|
||
if r["꼴"].startswith("확정")
|
||
and "다름" not in (st := [sb.checked(s) for s in sb.chosen_units(r)])
|
||
and "같음" in st
|
||
)
|
||
print(f"\n블록 {len(got)} · 꼴 {dict(kind)} · 단 {dict(tier)} · 품 같음 {same} · {sec:.1f} 초")
|
||
assert len(got) == 459
|
||
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 260
|
||
assert kind["확정·전부"] >= 52
|
||
assert same >= 141
|
||
assert tier["없음"] <= 94 and kind["없음"] <= 94
|
||
# 794-2c — 같은 말 묶음 · 옛 번호 대조표 · 기계 작업량 길(전 85 · 125 · 260)
|
||
assert tier["없음"] <= 58 and tier["확정"] >= 164
|
||
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 309
|
||
|
||
|
||
def _first(bid: str) -> dict:
|
||
return results()[0][bid]["근거후보"]
|
||
|
||
|
||
def test_plywood_four_same():
|
||
for bid in ("북부청임도#24", "북부청사방#66", "거창#41", "동부청영월#1841"):
|
||
(c,) = _first(bid)
|
||
assert (c["기초"], c["단"], c["품검산"]) == ("GC000211", "확정", "같음"), bid
|
||
assert {c["옵션"][k] for k in ("사용횟수", "유형")} == {"4회", "보통"}, bid
|
||
assert {"높이감률", "구멍땜"} <= set(c["입력필요"]), bid # 글에 없는 감률 = 입력 필요
|
||
|
||
|
||
def test_rebar_type2_cut_and_join():
|
||
got = {c["기초"]: c for c in _first("북부청임도#72")}
|
||
assert set(got) == {"GC000206", "GC000207"}
|
||
assert got["GC000206"]["옵션"]["Type"] == got["GC000207"]["옵션"]["Type"] == "Type-Ⅱ"
|
||
assert all(c["단"] == "확정" for c in got.values())
|
||
|
||
|
||
def test_remicon_small_50():
|
||
(c,) = _first("북부청임도#21")
|
||
assert c["기초"] == "GC000198" and c["단"] == "확정"
|
||
assert c["옵션"]["소량감률"] == 50 and c["옵션길"]["소량감률"] == "B5"
|
||
|
||
|
||
def test_excavation_51():
|
||
(c,) = _first("북부청임도#51")
|
||
assert (c["기초"], c["단"], c["옵션"]["Type"], c["옵션"]["깊이"]) == (
|
||
"GC000089",
|
||
"확정",
|
||
"TypeⅢ",
|
||
"5m 이하",
|
||
)
|
||
|
||
|
||
def test_rubble_carry_gc001140():
|
||
r = results()[0]["동부청영월#1466"]
|
||
c = r["근거후보"][0]
|
||
assert r["단"] == "사람" and c["기초"] == "GC001140"
|
||
assert c["옵션"] == {"재료갈래": "석재류", "운반로": "불량"}
|
||
assert set(c["옵션길"].values()) == {"V"}
|
||
|
||
|
||
def test_big_stone_near_unit_basis():
|
||
r = results()[0]["북부청임도#32"]
|
||
(c,) = r["근거후보"]
|
||
assert (r["단"], c["기초"], c["단"], c["품검산"]) == ("근접", "GF000226", "근접", "다름")
|
||
assert {t for t, _ in c["까닭"]} == {"단위 기준 차이"} and "÷10" in c["까닭"][0][1]
|
||
|
||
|
||
def test_no_made_up_values():
|
||
"""옵션마다 출처 꼬리표 · 입력 필요 · 0 간주 칸은 옵션에 없음 ·
|
||
없음 단은 근거후보 없이 보강 까닭만."""
|
||
for bid, r in results()[0].items():
|
||
for c in r["근거후보"]:
|
||
assert set(c["옵션"]) <= set(c.get("옵션길") or {}), bid
|
||
assert set((c.get("옵션길") or {}).values()) <= WAYS, bid
|
||
assert not set(c["옵션"]) & set(c["입력필요"]), bid
|
||
assert all(v is not None for v in c["옵션"].values()), bid
|
||
assert c["단"] in ("확정", "근접", "사람")
|
||
if r["단"] == "없음":
|
||
assert r["근거후보"] == [] and r["보강"]["까닭"], bid
|
||
|
||
|
||
def test_corpus_is_eight_files():
|
||
"""보정 자료 = 여덟 파일 상수(일반 뺌) — 뽑기 규칙을 바꾸면 다시 지어 넣음."""
|
||
preps = [
|
||
bc.prep(blk, book["꼴"], (book["숨은식"] or {}).get(str(blk["번"])))
|
||
for _, book in books()
|
||
for blk in book["단산"]
|
||
]
|
||
shipped = json.loads(bi.CORPUS.read_text(encoding="utf-8"))
|
||
assert sorted(map(json.dumps, bc.corpus(preps))) == sorted(map(json.dumps, shipped))
|
||
|
||
|
||
def test_store_round_trip(tmp_path):
|
||
"""임시 폴더 HP → 뒤 셈 저장본 · 목록 · 블록 하나 · 끝내기."""
|
||
_, book = books()[0]
|
||
small = {**book, "단산": [b for b in book["단산"] if b["번"] in (21, 51)]}
|
||
row = hopyo_store.save(tmp_path, "시험.xls", small, {"id": 0, "이름": "시험"})
|
||
key = row["키"]
|
||
assert store.solved(tmp_path, key) is None
|
||
got = store.solve_all(tmp_path, key)
|
||
assert set(got) == {"21", "51"} and store.solved(tmp_path, key) == json.loads(json.dumps(got))
|
||
assert store.listing(tmp_path, key)["셈중"] is False
|
||
assert store.block(tmp_path, key, 51)["근거후보"][0]["기초"] == "GC000089"
|
||
with pytest.raises(Exception):
|
||
store.block(tmp_path, key, 999)
|
||
store.forget(tmp_path, key)
|
||
assert store.solved(tmp_path, key) is None
|
||
judge.forget(tmp_path, key)
|
||
|
||
|
||
def test_hidden_g_lines():
|
||
book = {"꼴": "STmate", "단산": [{"번": "3", "행": [2, 4]}]}
|
||
rows = [[None] * 7, [None] * 6 + ["_x0007_L=20"], [None] * 6 + ["_x0007_"], [None] * 6 + ["x"]]
|
||
assert store.hidden(book, {store.SHEET: rows}) == {"3": ["L=20"]}
|
||
assert store.hidden({"꼴": "엑셀꼴", "단산": []}, {}) == {}
|
||
|
||
|
||
# ── 794-2c 손 확인 견본 ─────────────────────────────────────────────
|
||
HAND = [
|
||
("북부청임도#56", "GC001148", "확정"), # 잔토정리 Q 식 · 제목에 품셈 이름 없음 → 기계 작업량 길
|
||
("울진대흥#2264", "GC001148", "확정"), # 섞기 오인(GF000132) → 옛 11-3 · 굴착기
|
||
("울진대흥#2269", "GC001148", "확정"), # 섞기 오인 → 되메우기 굴착기 Q 식
|
||
("울진대흥#2290", "GC001148", "근접"), # 섞기 오인 → 적상 굴착기(운반 · 정지 절 미확정)
|
||
("동부청영월#111", "GC001189", "근접"), # 섞기 오인 → 옛 8-2-35 = 현행 8-2-32 임목파쇄기
|
||
("북부청임도#41", "GC000227", "확정"), # 휠라 = 채움재 · 옛 6-3-10 같은 절
|
||
("울진소광#1329", "GC000143", "확정"), # 평떼 = 잔디 · 옛 4-1-1 같은 절
|
||
("울진소광#794", "GF000298", "근접"), # 사방(옛) 4-16-2 → 판 대응표 산림 5-16-1
|
||
("동부청영월#33", "GC000129", "근접"), # 옛 3-7 → 현행 3-9-1 벌목(표 값 같음)
|
||
("울진대흥#2277", "GC001148", "확정"), # 옛 11-3 → 8-2-3 · 번호 + 검산
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize("bid,key,tier", HAND)
|
||
def test_794_2c_hand(bid, key, tier):
|
||
r = results()[0][bid]
|
||
keys = [c["기초"] for c in r["근거후보"]]
|
||
assert key in keys and r["단"] == tier, (bid, r["단"], keys)
|
||
assert "GF000132" not in keys, bid # 섞기(굴착기 0.7) 값 우연 짝 없음
|
||
|
||
|
||
def test_794_2c_no_invented_divisor():
|
||
"""「/ Q / 3」(1/3 적용) 블록은 검산 못 함 = 사람 · 1/3 을 지어 넣지 않음."""
|
||
r = results()[0]["봉화#24"]
|
||
assert r["단"] == "사람" and {c["기초"] for c in r["근거후보"]} == {"GC001147", "GC001148"}
|
||
assert all(c["옵션"] == {} for c in r["근거후보"])
|
||
|
||
|
||
def test_machine_consts_reader():
|
||
got = bm.consts(
|
||
[
|
||
"q = 0.7 , k = 0.7 , f =1/1.175= 0.85",
|
||
"E = 0.5 , Cm = 18 sec(90)",
|
||
"Q = 3600*q*k*E*f/Cm = 41.65 m3/hr",
|
||
]
|
||
)
|
||
assert got == [
|
||
{"기호": "q", "값": 0.7},
|
||
{"기호": "K", "값": 0.7},
|
||
{"기호": "f", "값": 0.85},
|
||
{"기호": "E", "값": 0.5},
|
||
{"기호": "Cm", "값": 18.0, "덧": "90˚"},
|
||
]
|
||
got = bm.consts(
|
||
[
|
||
"q (버킷용량(m3)) = 0.7 , f (체적환산계수) = 1/1.25 , K (버킷계수) = 0.9",
|
||
"Cm (1회 사이클 시간(초)) = 20 sec (135˚) , E = ( 0.55 + 0.45 ) / 2. - 0.05 = 0.45",
|
||
]
|
||
)
|
||
assert [(x["기호"], x["값"]) for x in got] == [
|
||
("q", 0.7),
|
||
("f", 0.8),
|
||
("K", 0.9),
|
||
("Cm", 20.0),
|
||
("E", 0.45),
|
||
]
|
||
assert bm.has_q(["3,600. × q × K × f × E", "Cm 20."]) and not bm.has_q(["Q = 6 ㎥/hr"])
|
||
|
||
|
||
def test_old_number_map():
|
||
assert {o["키"] for o in bm.old_rows("건설품셈(옛)", "11-3")} >= {"GC001147", "GC001148"}
|
||
assert "GC000129" in {o["키"] for o in bm.old_rows("건설품셈(옛)", "3-7")}
|
||
assert "GC001189" in {o["키"] for o in bm.old_rows("건설품셈(옛)", "8-2-35")}
|
||
assert bm.sabang_map()["4-16-2"] == "5-16-1"
|
||
assert "3-7" not in bm.sabang_map() # 임도(2017) 줄은 사방에 안 씀
|
||
assert bm.old_rows("건설품셈(옛)", "8-2") == [] # 대조 없는 번호 = 없음(윗마디 안 받음)
|