- 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>
145 lines
6.4 KiB
Python
145 lines
6.4 KiB
Python
"""M02 일위대가 실무자료 단가산출 근거(PLAN 330) — 엑셀 「단가산출」 블록 → 출처 · 직종 ·
|
|
우리 돈 로직과 견준 근거 후보.
|
|
|
|
읽기(`read` · `sources` · `trade_names` · `SHEET` · `GLINES` · `GCHARS`) = EST 어댑터
|
|
`M02_Hopyo_ReadEst`(776-2 · 이름은 그대로 다시 냄).
|
|
출처 풀이 · 조항 · 기초 없음(`settle` · `resolve` · `ours` · `clause` · `no_basis` …) = 785-1 부터
|
|
`M02_Hopyo_Clause` 정본(여기는 다시 냄 · 옛 맞춤 층이 지워질 때 785-7 이 걷음).
|
|
견줌(`candidates`) = 같은 책(부문을 알면 같은 구분) 조항 같음 1.0 ·
|
|
윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) ·
|
|
앞 마디 같음 0.5(조항이 안 맞으면 후보 아님 · `clause`) · 조항 오타 「13-114」 = 「13-11-4」 ·
|
|
건설 공통 · 토목 덤 0.1 · 직종 겹침(보통인부 뺌) 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 ·
|
|
같은 점수는 글자 두 개 묶음(괄호 글 포함)이 가름 · 이름 · 낱말(괄호 글 포함 · 인력 · 기계 뺌)이
|
|
하나도 안 겹치는 근거 후보는(근거 맞음 = 조항 · 직종 · 단위 다 맞으면 빼고) 겹치는 근거 후보 뒤로 ·
|
|
다섯 안에 이름 1순위 하나는 늘 남김(`pick`).
|
|
자동 확정 없음 — 맞춤 차례는 `M02_Hopyo_Match`.
|
|
M01 · 원본 로직은 읽기만.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match # 서로 부름 — 부를 때만 씀
|
|
from M02_MasterTemplete.M02_Hopyo_Clause import ( # noqa: F401 — 785-1 정본 · 옛 이름 그대로
|
|
OLD,
|
|
_flat,
|
|
_grams,
|
|
_has,
|
|
_head,
|
|
_named,
|
|
_ours,
|
|
_part,
|
|
_sections,
|
|
clause,
|
|
has_logic,
|
|
no_basis,
|
|
ours,
|
|
resolve,
|
|
road_map,
|
|
settle,
|
|
)
|
|
from M02_MasterTemplete.M02_Hopyo_ReadEst import ( # noqa: F401 — 읽기는 EST 어댑터(776-2) · 옛 이름 그대로
|
|
GCHARS,
|
|
GLINES,
|
|
SHEET,
|
|
read,
|
|
sources,
|
|
trade_names,
|
|
)
|
|
|
|
COMMON = "보통인부"
|
|
BONUS_PARTS = ("공통", "토목")
|
|
_TYPOS = (("깍기", "깎기"),)
|
|
_PLAIN_WORDS = frozenset({"인력", "기계", "장비"}) # 방법 낱말 — 「터널굴착 1발파당 인력」
|
|
|
|
|
|
def _text(line: dict[str, Any]) -> str:
|
|
text = f"{line.get('이름') or ''} {line.get('규격') or ''}"
|
|
for old, new in _TYPOS:
|
|
text = text.replace(old, new)
|
|
return text
|
|
|
|
|
|
def _loose_words(text: str) -> frozenset[str]:
|
|
"""괄호 글까지 뜻 낱말 — 방법 낱말 뺌."""
|
|
return hopyo_match.words(text.replace("(", " ").replace(")", " ")) - _PLAIN_WORDS
|
|
|
|
|
|
def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""근거 후보 — 줄 `근거.출처` 가 없으면 빈 것 · `{로직, 이름, 단위, 근거, _겹침, _낱말}`
|
|
점수 차례(`_` 칸은 `merge` 가 쓰고 뺌)."""
|
|
basis = line.get("근거") or {}
|
|
theirs = [list(one) for one in basis.get("출처") or []]
|
|
if not theirs:
|
|
return []
|
|
trades = set(basis.get("직종") or []) - {COMMON}
|
|
text = _text(line)
|
|
mine = hopyo_match.words(text) # 괄호 글 뺌 — 「잔토처리(인력)」 ↔ 「철골재 철거(인력)」
|
|
grams = hopyo_match._grams(hopyo_match.mm.plain(text)) # 괄호 글 포함 — 같은 점수 가름
|
|
loose = _loose_words(text)
|
|
table = ours()
|
|
scored = []
|
|
for one in got["로직"]:
|
|
our = table.get(one["로직"])
|
|
if not our:
|
|
continue
|
|
score, clauses, tag = clause(theirs, our)
|
|
if not score:
|
|
continue
|
|
shared = sorted(trades & our["직종"])
|
|
if not shared and not any(a in b or b in a for a in mine for b in one["words"]):
|
|
continue
|
|
tags = [tag]
|
|
if our["책"] == "건설품셈" and our["부문"] in BONUS_PARTS:
|
|
score += 0.1
|
|
tags.append(f"건설 {our['부문']}")
|
|
if shared:
|
|
score += 0.5
|
|
tags.append("직종 " + " · ".join(shared))
|
|
unit = hopyo_match.fits(line.get("단위"), one["단위"])
|
|
if unit:
|
|
score += 0.15
|
|
tags.append("단위 맞음")
|
|
dice = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"]))
|
|
why = {"조항": clauses, "직종": shared, "단위": unit, "점수": round(score, 3)}
|
|
theirs_loose = _loose_words(one["이름"])
|
|
overlap = any(a in b or b in a for a in loose for b in theirs_loose)
|
|
full = tag != "앞 마디 같음" and unit and (bool(shared) or not trades) # 근거 맞음
|
|
why["까닭"] = " · ".join(tags)
|
|
extra = {"_겹침": dice, "_낱말": overlap or full}
|
|
scored.append((score, dice, {**hopyo_match._public(one), "근거": why, **extra}))
|
|
scored.sort(key=lambda x: (-x[0], -x[1]))
|
|
return [one for _, _, one in scored]
|
|
|
|
|
|
def merge(basis: list[dict[str, Any]], named: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""근거 후보 + 이름 후보 → 로직 키로 합침(점수 = 이름 점수 + 근거 점수) · 점수 차례 ·
|
|
같으면 이름 겹침 큰 쪽 · 이름 · 낱말 안 겹치는 근거 후보는 겹치는 근거 후보 뒤로."""
|
|
if not basis:
|
|
return named
|
|
by_name = {c.get("로직"): c["점수"] for c in named}
|
|
out = []
|
|
for c in basis:
|
|
named_too = c["로직"] in by_name
|
|
score = round(c["근거"]["점수"] + by_name.pop(c["로직"], 0), 3)
|
|
out.append({**c, "점수": score, "_낱말": c["_낱말"] or named_too})
|
|
out += [c for c in named if c.get("로직") in by_name]
|
|
out.sort(key=lambda c: (-c["점수"], -c.get("_겹침", 0)))
|
|
loose = [c for c in out if c.get("_낱말") is False]
|
|
if loose and any(c.get("_낱말") for c in out):
|
|
out = [c for c in out if c.get("_낱말") is not False]
|
|
last = max(at for at, c in enumerate(out) if c.get("_낱말"))
|
|
out[last + 1 : last + 1] = loose
|
|
return [{k: v for k, v in c.items() if not k.startswith("_")} for c in out]
|
|
|
|
|
|
def pick(ordered: list[dict[str, Any]], first: Any, room: int) -> list[dict[str, Any]]:
|
|
"""후보 `room` 개 — 이름 1순위(`first` 로직 키) 하나는 늘 남기고 나머지는 차례대로."""
|
|
if room <= 0:
|
|
return []
|
|
if first is None or all(c.get("로직") != first for c in ordered):
|
|
return ordered[:room]
|
|
others = [c for c in ordered if c.get("로직") != first][: room - 1]
|
|
return [c for c in ordered if c.get("로직") == first or any(c is o for o in others)]
|