Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -156,7 +156,9 @@ _PER_LENGTH_UNITS = frozenset({"m"})
|
||||
#: ㉯ 양식에 **그 이름 줄이 없으면 전개 값을 남기는** 성분(2026-09-14 브레인 판정 · 목록으로 못박음).
|
||||
#: STmate 호표(고정형)는 토공·버림을 안 품음 — 통째로 갈음하면 구조물터파기 85.25→25㎥(−6,037,712원 실측).
|
||||
#: 목록 밖 성분은 양식으로 갈음 · 목록 안이라도 양식이 그 줄을 품으면 양식 값(겹쳐 세지 않음).
|
||||
KEEP_ENGINE_COMPONENTS = ("터파기", "되메우기", "잔토처리", "버림콘크리트")
|
||||
#: ② 실무 엑셀 벽·돌쌓기 호표 57 전수 — 기초잡석 0/57(「기초다짐 및 뒤채움」 4 은 다른 것) ·
|
||||
#: 채집석 = 사토에서 빼는 돌 부피(haul_deduction)라 호표의 채집 줄(품 · 41/57)과 다른 것.
|
||||
KEEP_ENGINE_COMPONENTS = ("터파기", "되메우기", "잔토처리", "버림콘크리트", "기초잡석", "채집석")
|
||||
KEPT_ROW_REASON = "전개 값 그대로 — 양식이 이 줄을 안 품음(토공·버림은 전개가 셈)"
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
@@ -362,32 +363,64 @@ def save_manual(
|
||||
return merged
|
||||
|
||||
|
||||
#: 조각 후보의 최소 겹침 글자 수(2026-09-14 브레인 판정 ① — 「채집」·「타설」이 걸리게).
|
||||
MIN_FRAGMENT = 2
|
||||
|
||||
|
||||
def _longest_common(a: str, b: str) -> int:
|
||||
"""두 글의 가장 긴 공통 조각 길이."""
|
||||
best = 0
|
||||
previous = [0] * (len(b) + 1)
|
||||
for char in a:
|
||||
current = [0]
|
||||
for index, other in enumerate(b):
|
||||
current.append(previous[index] + 1 if char == other else 0)
|
||||
best = max(best, max(current))
|
||||
previous = current
|
||||
return best
|
||||
|
||||
|
||||
def search_titles(book: Any, query: str, kind: str, limit: int = 50) -> list[dict[str, Any]]:
|
||||
"""고르개 — 단가표에서 낱말이 **모두** 든(코드·이름·규격) 항목. 단가가 섰는지도 함께."""
|
||||
"""고르개 — 단가표 항목 후보. 단가가 섰는지도 함께.
|
||||
|
||||
낱말이 **모두** 든(코드·이름·규격) 항목이 먼저, 그다음 **두 글자 이상 조각이 겹친** 항목을
|
||||
겹친 글자 수 순으로(①). 고정형 줄 이름(깬잡석채집)과 단가표 제목(막돌 채집)은 낱말이 달라
|
||||
전부 포함 방식으로는 후보가 0 이었음. ⚠ 후보만 — 고르는 것은 사람(명세 2장 · 별칭표로 안 맞춤).
|
||||
"""
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceBookError
|
||||
|
||||
kinds = SEARCH_KINDS[kind]
|
||||
words = query.lower().split()
|
||||
found: list[dict[str, Any]] = []
|
||||
for title in book.titles.values() if words else ():
|
||||
if len(found) >= limit:
|
||||
break
|
||||
# 조각 비교는 글자·숫자만 — 괄호 「(장비)」 같은 기호가 겹친 글자 수를 부풀리지 않게.
|
||||
compact = re.sub(r"[\W_]+", "", query.lower())
|
||||
scored: list[tuple[int, int, int, Any]] = []
|
||||
for order, title in enumerate(book.titles.values() if words else ()):
|
||||
if title.kind.value not in kinds:
|
||||
continue
|
||||
text = f"{title.code} {title.name} {title.spec}".lower()
|
||||
if all(word in text for word in words):
|
||||
try:
|
||||
money = book.resolve(title.code)
|
||||
total: float | None = float(money.material + money.labor + money.expense)
|
||||
except PriceBookError:
|
||||
total = None
|
||||
found.append(
|
||||
{
|
||||
"code": title.code,
|
||||
"name": title.name,
|
||||
"spec": title.spec,
|
||||
"unit": _unit(title.unit),
|
||||
"price": total,
|
||||
}
|
||||
)
|
||||
scored.append((1, len(compact), order, title))
|
||||
continue
|
||||
overlap = _longest_common(
|
||||
compact, re.sub(r"[\W_]+", "", f"{title.name}{title.spec}".lower())
|
||||
)
|
||||
if overlap >= MIN_FRAGMENT:
|
||||
scored.append((0, overlap, order, title))
|
||||
scored.sort(key=lambda item: (-item[0], -item[1], item[2]))
|
||||
found: list[dict[str, Any]] = []
|
||||
for _whole, _overlap, _order, title in scored[:limit]:
|
||||
try:
|
||||
money = book.resolve(title.code)
|
||||
total: float | None = float(money.material + money.labor + money.expense)
|
||||
except PriceBookError:
|
||||
total = None
|
||||
found.append(
|
||||
{
|
||||
"code": title.code,
|
||||
"name": title.name,
|
||||
"spec": title.spec,
|
||||
"unit": _unit(title.unit),
|
||||
"price": total,
|
||||
}
|
||||
)
|
||||
return found
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""고르개 후보 — 두 글자 이상 조각이 겹치면 띄움 (2026-09-14 브레인 판정 ①).
|
||||
|
||||
실측(936be972): 고정형 줄 이름 그대로(깬잡석채집·고임돌채집…)로 찾으면 **후보 0** — 단가표 제목
|
||||
(「막돌 채집」·「레디믹스트콘크리트 타설」)과 낱말이 달라 「낱말 전부 포함」 방식으로는 영영 안 맞음.
|
||||
⇒ 겹친 조각(「채집」·「타설」)으로 넓게 띄우고 **겹친 글자 수 순**. 고르는 것은 사람(별칭표로 안 맞춤 · 명세 2장).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from types import SimpleNamespace
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import search_titles
|
||||
from B09_Estimation.B09_Estimation_PriceBook import Money3
|
||||
|
||||
|
||||
def _title(code: str, name: str, spec: str, unit: str) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
code=code, name=name, spec=spec, unit=unit, kind=SimpleNamespace(value="unit_price")
|
||||
)
|
||||
|
||||
|
||||
class Book:
|
||||
titles = {
|
||||
t.code: t
|
||||
for t in (
|
||||
_title("B-1", "막돌 채집", "㎡당", "㎡"),
|
||||
_title("B-2", "레디믹스트콘크리트 타설", "무근구조물", "㎥"),
|
||||
_title("B-3", "고임돌 채집", "기계", "㎥"),
|
||||
_title("B-4", "견치돌 찰쌓기", "뒷길이 35㎝", "㎡"),
|
||||
_title("B-5", "표토 제거", "", "㎡"),
|
||||
)
|
||||
}
|
||||
|
||||
def resolve(self, code: str) -> Money3:
|
||||
return Money3(Decimal(1), Decimal(0), Decimal(0))
|
||||
|
||||
|
||||
def _codes(query: str) -> list[str]:
|
||||
return [item["code"] for item in search_titles(Book(), query, "work")]
|
||||
|
||||
|
||||
def test_이름_그대로도_겹친_조각으로_후보가_뜬다() -> None:
|
||||
assert _codes("깬잡석채집")[:1] and set(_codes("깬잡석채집")) >= {"B-1", "B-3"}
|
||||
assert _codes("레미콘타설(장비)") == ["B-2"]
|
||||
assert _codes("깬잡석찰쌓기") == ["B-4"]
|
||||
|
||||
|
||||
def test_겹친_글자_수_순() -> None:
|
||||
# 「고임돌채집」 — 고임돌 채집(5자 겹침) · 막돌 채집(「돌채집」 3자) 차례
|
||||
assert _codes("고임돌채집")[:2] == ["B-3", "B-1"]
|
||||
|
||||
|
||||
def test_한_글자만_겹치면_안_띄운다() -> None:
|
||||
assert "B-5" not in _codes("깬잡석채집") # 겹침 없음
|
||||
assert "B-4" not in _codes("고임돌채집") # 「돌」 한 글자만 겹침
|
||||
|
||||
|
||||
def test_종전_낱말_검색도_그대로() -> None:
|
||||
assert _codes("찰쌓기 35") == ["B-4"]
|
||||
assert set(_codes("돌")) == {"B-1", "B-3", "B-4"} # 낱말이 제목에 든 것은 종전대로
|
||||
@@ -60,13 +60,17 @@ def _fixed() -> dict:
|
||||
|
||||
def test_목록이_코드에_못박혀_있다() -> None:
|
||||
assert set(KEEP_ENGINE_COMPONENTS) >= {"터파기", "되메우기", "잔토처리", "버림콘크리트"}
|
||||
# ② 원문 빈도(벽·돌쌓기 호표 57) — 기초잡석 0/57 · 채집석(사토 공제 부피)은 호표 채집 줄(품)과 다른 것
|
||||
assert {"기초잡석", "채집석"} <= set(KEEP_ENGINE_COMPONENTS)
|
||||
|
||||
|
||||
def test_고정형을_가져와도_토공_버림_성분은_남는다() -> None:
|
||||
engine = build_table([WALL], NAMES, {}, {}, None, use_templates=False)["structures"][0]
|
||||
fixed = build_table([WALL], NAMES, {}, {}, None, structure_templates={"masonry_wet": _fixed()})
|
||||
components = {c["name"]: c for c in fixed["structures"][0]["components"]}
|
||||
for name in ("터파기", "되메우기", "잔토처리", "버림콘크리트"):
|
||||
engine_names = {c["name"] for c in engine["components"]}
|
||||
assert {"기초잡석", "채집석"} <= engine_names # 시험 벽에서 둘 다 서야 보존을 잼
|
||||
for name in ("터파기", "되메우기", "잔토처리", "버림콘크리트", "기초잡석", "채집석"):
|
||||
before = next(c for c in engine["components"] if c["name"] == name)
|
||||
assert components[name]["amount"] == before["amount"], name
|
||||
assert "깬잡석찰쌓기" in components and "돌" not in components # 목록 밖은 양식으로 갈음
|
||||
|
||||
Reference in New Issue
Block a user