feat(746-1): 불러오기 고름 대기 후보 블록 줄비교 · 못옮김 · 기계 줄 M01 요소 후보 · 단위 별칭 hr = 시간 · 개소 = 개
- 고름 대기 후보 앞 셋에 그 기초와 줄마다 견준 `줄비교`(같음 · 품다름 · 엑셀만 · 기초만 · 1 단위 품 · 못옮김) — 고르면 만드는 SK 와 같은 셈(`_Plan` 기록) · 판정 · 순위 · 미리 고름 그대로 - 못옮김 `후보` · 안 이은 기계 줄 `남은줄[].후보` = M01 요소 셋(새 `M02_Hopyo_Elem` — 이름 · 규격 점수 › 엑셀 단가 가까운 차례) - 단위 모듈 짝(master_unit.py ↔ master_unit_swap.ts) 별칭 hr = 시간 · 개수 묶음에 개소 — 거울 시험 사례 셋 더함 - 실측(여덟 파일): 고름 대기 89 중 1위 줄비교 72(셋 중 하나 82) · 골든 1위 163 그대로 · 판정 수 그대로 · 개 ↔ 개소 1 블록 견줌 풀림 · 못옮김 후보 8/9 · 기계 줄 후보 32/32 · M01 로직 3,992 값 전후 같음 - 새 지킴 시험 test_746_1_hopyo_compare Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,13 +13,14 @@ const ALIAS: Record<string, string> = {
|
||||
리터: "ℓ",
|
||||
set: "조",
|
||||
세트: "조",
|
||||
hr: "시간",
|
||||
};
|
||||
const UNITS: Record<string, Record<string, number>> = {
|
||||
무게: { g: 1, kg: 1000, 톤: 1000000 },
|
||||
길이: { mm: 1, cm: 10, m: 1000, km: 1000000 },
|
||||
넓이: { cm2: 1, m2: 10000 },
|
||||
부피: { cm3: 1, ℓ: 1000, m3: 1000000 },
|
||||
개수: { 개: 1, 본: 1, 매: 1 },
|
||||
개수: { 개: 1, 본: 1, 매: 1, 개소: 1 },
|
||||
};
|
||||
const SCALE: Record<string, [string, number]> = {};
|
||||
for (const [kind, group] of Object.entries(UNITS)) {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""M02 일위대가 불러오기 — 못옮김 · 안 이은 기계 줄 → M01 요소 후보 셋(PLAN 746-1 · 요구 585).
|
||||
|
||||
후보 = `[{키, 갈래 노임|기계|자재, 이름, 규격, 단위, 단가}]` 셋까지 — 보여주기만(자동 이음 없음 · 고르면 지금
|
||||
잇기 길 `{노임|기계|자재: 키}`).
|
||||
자재 = `M02_Hopyo_Material.candidates` 점수(이름 글자 묶음 · 규격) · 단가 = M01 자재 값(엑셀 줄 단위)
|
||||
노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값
|
||||
기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 단가 없음(기계경비는 로직 셈)
|
||||
차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 글 「이름 단가 × 식 = 금액」 · 모르면 점수만).
|
||||
서버 단독 · 읽기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, parse_rhs, specnums
|
||||
|
||||
TOP = 3
|
||||
POOL = 5
|
||||
|
||||
|
||||
def prices(block: dict[str, Any] | None) -> dict[str, float]:
|
||||
"""블록 글 → `{이름: 엑셀 단가}`(직종 줄 「직종 : 단가 × 식」 · 자재 줄 「이름 단가 × 식 = 금액」)."""
|
||||
out: dict[str, float] = {}
|
||||
for raw in (block or {}).get("글") or []:
|
||||
row = str(raw).rstrip()
|
||||
cm = finger._COLON.match(row)
|
||||
mm = None if cm else finger._MAT.match(row)
|
||||
name = (cm or mm).group("name").strip() if (cm or mm) else ""
|
||||
got = parse_rhs((cm or mm).group("rhs")) if name else None
|
||||
if got and got.get("단가"):
|
||||
out.setdefault(re.sub(r"^[0-9가-힣]\.", "", name).strip(), float(got["단가"]))
|
||||
return out
|
||||
|
||||
|
||||
def _gap(price: Any, want: float | None) -> float:
|
||||
try:
|
||||
p = float(price)
|
||||
except (TypeError, ValueError):
|
||||
return math.inf
|
||||
return abs(math.log(p / want)) if want and p > 0 else math.inf
|
||||
|
||||
|
||||
def _order(rows: list[tuple[float, dict]], want: float | None) -> list[dict]:
|
||||
rows.sort(key=lambda x: -x[0])
|
||||
pool = rows[:POOL]
|
||||
pool.sort(key=lambda x: (-round(x[0], 1), _gap(x[1].get("단가"), want)))
|
||||
return [one for _, one in pool[:TOP]]
|
||||
|
||||
|
||||
def _num(x: Any) -> int | float | None:
|
||||
if x is None:
|
||||
return None
|
||||
f = float(x)
|
||||
return int(f) if f == int(f) else round(f, 4)
|
||||
|
||||
|
||||
def materials(name: str, unit: str = "", want: float | None = None) -> list[dict[str, Any]]:
|
||||
whole = m01.loaded()[1]
|
||||
rows = []
|
||||
for c in hmat.candidates({"이름": name, "규격": "", "단위": unit}, POOL):
|
||||
code = c.get("자재")
|
||||
if not code:
|
||||
continue
|
||||
try:
|
||||
price = hmat.mm.element(whole, code, {}, unit or None)[0]
|
||||
except Exception: # noqa: BLE001 — 값 못 읽음 = 단가 없음
|
||||
price = None
|
||||
one = {"키": code, "갈래": "자재", "이름": c.get("이름"), "규격": c.get("규격") or ""}
|
||||
rows.append((c.get("점수") or 0.0, {**one, "단위": c.get("단위"), "단가": _num(price)}))
|
||||
return _order(rows, want)
|
||||
|
||||
|
||||
def labor(name: str, want: float | None = None) -> list[dict[str, Any]]:
|
||||
whole = m01.loaded()[1]
|
||||
me = hm._grams(re.sub(r"\s", "", name))
|
||||
rows = []
|
||||
for row in whole.numbers.get("LB", {}).values():
|
||||
them = hm._grams(re.sub(r"\s", "", str(row.get("이름") or "")))
|
||||
score = 2 * len(me & them) / (len(me) + len(them))
|
||||
if score > 0:
|
||||
one = {"키": str(row.get("키")), "갈래": "노임", "이름": row.get("이름"), "규격": ""}
|
||||
rows.append((score, {**one, "단위": "인", "단가": _num(row.get("값"))}))
|
||||
return _order(rows, want)
|
||||
|
||||
|
||||
def machines(text: str, kinds: set | None = None, nums: set | None = None) -> list[dict]:
|
||||
"""기계 글(이름 규격) → EQ 후보 — 갈래 · 규격 수가 없으면 글에서 읽음."""
|
||||
whole = m01.loaded()[1]
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
mine = hm.words(text)
|
||||
rows = []
|
||||
for row in whole.numbers.get("EQ", {}).values():
|
||||
types = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
|
||||
if not kinds & types:
|
||||
continue
|
||||
spec = set(specnums(row.get("규격")))
|
||||
score = len(kinds & types) / len(kinds | types)
|
||||
score += 1.0 if nums and nums == spec else 0.5 if nums & spec else 0.0
|
||||
score += 0.3 * len(mine & hm.words(row.get("이름")))
|
||||
one = {"키": str(row.get("키")), "갈래": "기계", "이름": row.get("이름")}
|
||||
unit = (row.get("단위") or {}).get("내용시간") if isinstance(row.get("단위"), dict) else ""
|
||||
rows.append(
|
||||
(score, {**one, "규격": row.get("규격") or "", "단위": unit or "시간", "단가": None})
|
||||
)
|
||||
return _order(rows, None)
|
||||
|
||||
|
||||
def lost(one: dict[str, Any], want: float | None = None) -> list[dict[str, Any]]:
|
||||
"""못옮김 한 줄(`M02_Hopyo_SkPatch` 꼴) → 후보."""
|
||||
kind, name = one.get("갈래"), str(one.get("이름") or "")
|
||||
if kind == "자재":
|
||||
return materials(name, str(one.get("단위") or ""), want)
|
||||
if kind == "노임":
|
||||
return labor(name, want)
|
||||
if kind == "기계":
|
||||
return machines(f"{name} {one.get('규격') or ''}", set(name.split("·")))
|
||||
return []
|
||||
@@ -15,6 +15,7 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
@@ -118,10 +119,13 @@ def kept(
|
||||
|
||||
|
||||
def left(line: dict[str, Any], why: dict | None) -> dict[str, Any]:
|
||||
"""남은 줄 한 줄 — 엑셀 줄 · 갈래 · 까닭 · 후보."""
|
||||
"""남은 줄 한 줄 — 엑셀 줄 · 갈래 · 까닭 · 후보(기계 = M01 EQ 셋 · 746-1)."""
|
||||
kind = finger.kind_of(line)
|
||||
keep = ("차례", "이름", "규격", "단위", "수량")
|
||||
out = {**{k: line.get(k) for k in keep}, "갈래": kind}
|
||||
out["까닭"] = (why or {}).get("까닭") or WHY.get(kind, "이을 것 없음")
|
||||
more = {k: v for k, v in (why or {}).items() if k not in ("까닭", "후보")}
|
||||
return {**out, "후보": (why or {}).get("후보") or [], **more}
|
||||
cands = (why or {}).get("후보") or []
|
||||
if not cands and kind == "기계":
|
||||
cands = elem.machines(f"{line.get('이름') or ''} {line.get('규격') or ''}")
|
||||
return {**out, "후보": cands, **more}
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
`[{id, 노임|기계|자재: 키}]` = SK 더한줄 인력 · 기계 · 재료(엑셀 수량 그대로) · `{id, 뺌: 까닭}` = 놓음.
|
||||
SK 단위(728-2) = 엑셀 줄 단위 · 바탕 기준단위와 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비
|
||||
둘 이상이 같은 비일 때만 · `환산` {계수}) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」).
|
||||
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초).
|
||||
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초) ·
|
||||
앞 셋 `줄비교`(746-1) = 그 기초로 견준 줄마다 꼴(같음 · 품다름 · 엑셀만 · 기초만 · 1 단위 품) · 못옮김 ·
|
||||
견줌 못함(단위 성격 다름) = null — 판정 · 순위 · 미리 고름은 안 바꿈.
|
||||
못옮김 `후보` = M01 요소 셋(`M02_Hopyo_Elem` · 보여주기만).
|
||||
cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫
|
||||
(1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배.
|
||||
품 견줌 = 우리 셈(`M02_Sangeun_Calc.run` 줄 수량 ÷ 배) · 같음 = `Option.near`(0.5 % + 엑셀 자릿수 반올림).
|
||||
@@ -27,6 +30,7 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Combine as combine
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||||
@@ -40,6 +44,8 @@ KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김")
|
||||
TINY = 1e-9
|
||||
_ADD = re.compile(r"^add:#(\d+)$")
|
||||
UNIT_WHY = "단위 성격 다름 · 계수 필요"
|
||||
LOOKS = ("같음", "품다름", "엑셀만", "기초만") # 줄비교 꼴(746-1)
|
||||
COMPARE = 3 # 고름 대기 후보 앞 몇에 줄비교
|
||||
LINK = {
|
||||
"노임": ("인력", "LB"),
|
||||
"기계": ("기계", "EQ"),
|
||||
@@ -96,7 +102,7 @@ def parts(doc: dict, whole: Any) -> list[dict[str, Any]]:
|
||||
if kind == "인력":
|
||||
one.update(갈래="인력", 이름=labor.get(str(item.get("요소")), str(item.get("이름"))))
|
||||
elif kind == "재료":
|
||||
one.update(갈래="자재", 이름=str(item.get("이름") or ""))
|
||||
one.update(갈래="자재", 이름=str(item.get("이름") or ""), 단위=item.get("단위") or "")
|
||||
elif kind == "로직" and found:
|
||||
sub = found.group(1)
|
||||
if ours.is_mach_logic(whole.logic(sub)):
|
||||
@@ -138,6 +144,27 @@ class _Plan:
|
||||
self.lines = parts(doc, whole)
|
||||
self.lost: list[dict[str, Any]] = []
|
||||
self.lb = {name: key for key, name in ours._labor_names().items()}
|
||||
self.rows: list[dict[str, Any]] = [] # 줄비교(746-1)
|
||||
self.prices: dict[str, float] = {} # 엑셀 단가(못옮김 후보 차례)
|
||||
|
||||
def note(self, kind: str, name: str, excel: float, mine: float | None, **more: Any) -> None:
|
||||
"""줄비교 한 줄 — 꼴 = 같음 · 품다름 · 엑셀만 · 기초만(`꼴` 주면 그대로)."""
|
||||
look = more.pop("꼴", None)
|
||||
if look is None:
|
||||
if excel and mine:
|
||||
look = LOOKS[0] if _near(excel, mine) else LOOKS[1]
|
||||
else:
|
||||
look = LOOKS[2] if excel else LOOKS[3]
|
||||
num = lambda x: _num(float(f"{x:.10g}")) if x else None # noqa: E731
|
||||
one = {
|
||||
"갈래": kind,
|
||||
"이름": name,
|
||||
"규격": "",
|
||||
"단위": "",
|
||||
"엑셀": num(excel),
|
||||
"기초": num(mine),
|
||||
}
|
||||
self.rows.append({**one, **more, "꼴": look})
|
||||
|
||||
def cover(self, cid: str, **one: float) -> None:
|
||||
self.doc["덮개"][cid] = {k: _num(v) for k, v in one.items()}
|
||||
@@ -155,7 +182,8 @@ class _Plan:
|
||||
n = sum(1 for x in self.lost if (x["갈래"], x["이름"]) == (kind, name))
|
||||
one = {"id": base if not n else f"{base}#{n}", "갈래": kind, "이름": name, "규격": "",
|
||||
"단위": "", "수량": _num(float(f"{qty:.10g}")), "글": text, "후보": []} # fmt: skip
|
||||
self.lost.append({**one, **more})
|
||||
one = {**one, **more}
|
||||
self.lost.append({**one, "후보": elem.lost(one, self.prices.get(name))})
|
||||
|
||||
def link(self, picks: list[dict[str, Any]]) -> None:
|
||||
"""사용자 잇기 — 이은 줄 = 더한줄(엑셀 수량 × 배) · 뺌 = 놓음 · 나머지 못옮김 그대로."""
|
||||
@@ -191,6 +219,7 @@ class _Plan:
|
||||
for t in sorted(set(excel) | {x["이름"] for x in mine if x["품"]} | set(extra)):
|
||||
a, rows = excel.get(t, 0.0), [x for x in mine if x["이름"] == t]
|
||||
b, f = sum(x["품"] for x in rows), extra.get(t, 0.0)
|
||||
self.note("인력", t, a, b + f, 단위="인")
|
||||
if a and _near(a, b + f):
|
||||
continue
|
||||
want = a - f
|
||||
@@ -224,10 +253,11 @@ class _Plan:
|
||||
and _near(h, x["품"])), None) # fmt: skip
|
||||
if hit is not None:
|
||||
used.add(hit["cid"])
|
||||
self._mnote(m, h, hit["품"])
|
||||
continue
|
||||
at = next((i for i, (a, q) in enumerate(fixed) if a & kinds and _near(h, q)), None)
|
||||
if at is not None:
|
||||
fixed.pop(at)
|
||||
self._mnote(m, h, fixed.pop(at)[1])
|
||||
continue
|
||||
rest.append(m)
|
||||
for m in rest:
|
||||
@@ -236,15 +266,23 @@ class _Plan:
|
||||
same.sort(key=lambda x: not (sp and sp & x["규격"]))
|
||||
if same:
|
||||
used.add(same[0]["cid"])
|
||||
self._mnote(m, m["시간"], same[0]["품"])
|
||||
self.cover(same[0]["cid"], qty=m["시간"] * self.tb)
|
||||
else:
|
||||
self._mnote(m, m["시간"], None)
|
||||
self._add_machine(m, line)
|
||||
for x in mine:
|
||||
if x["cid"] not in used:
|
||||
self._mnote({"갈래": sorted(x["종류"]), "규격": sorted(x["규격"])}, 0.0, x["품"])
|
||||
self.drop(x["cid"])
|
||||
for a, q in fixed:
|
||||
self._mnote({"갈래": sorted(a)}, 0.0, q)
|
||||
self.miss("기계", "·".join(sorted(a)), q, "우리만 장비 — 더한 기초 안", 단위="시간")
|
||||
|
||||
def _mnote(self, m: dict[str, Any], excel: float, mine: float | None) -> None:
|
||||
spec = " ".join(f"{x:g}" for x in sorted(m.get("규격") or []))
|
||||
self.note("장비", "·".join(sorted(m["갈래"])), excel, mine, 규격=spec, 단위="시간")
|
||||
|
||||
def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None:
|
||||
"""엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기)."""
|
||||
fp = finger.empty(line)
|
||||
@@ -267,11 +305,14 @@ class _Plan:
|
||||
for m in excel:
|
||||
words = option.mat_words(m["이름"])
|
||||
rows = [x for x in mine if option._named(words, x["이름"])]
|
||||
unit = next((x["단위"] for x in rows if x.get("단위")), "")
|
||||
if rows and all(x["품"] is None for x in rows): # 우리 = 별도계상 · 엑셀 = 블록 안
|
||||
self.note("자재", m["이름"], m["품"], None, 단위=unit, 꼴=LOOKS[1])
|
||||
self.doc["자재표시"][rows[0]["cid"]] = {"별도계상": "직접계산"}
|
||||
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
|
||||
continue
|
||||
rows = [x for x in rows if x["품"] is not None]
|
||||
self.note("자재", m["이름"], m["품"], sum(x["품"] for x in rows) or None, 단위=unit)
|
||||
if rows and _near(m["품"], sum(x["품"] for x in rows)):
|
||||
continue
|
||||
if rows:
|
||||
@@ -309,11 +350,29 @@ def _need(code: str, unit: str, whole: Any) -> dict[str, Any] | None:
|
||||
return None if swap["배"] is not None else {"가": got[0], "나": unit, "갈래": swap["계수"]}
|
||||
|
||||
|
||||
def _wait(entry: dict, whole: Any, unit: str, why: str | None = None) -> dict[str, Any]:
|
||||
def compare(
|
||||
entry: dict, block: dict | None, line: dict, whole: Any, code: str, efp: dict | None = None
|
||||
) -> dict | None:
|
||||
"""고른 기초 `code` 로 견준 줄비교 `{줄, 수, 못옮김, 환산?}`(만들지 않음) · 견줌 못함 = None."""
|
||||
got = plan(entry, block, line, whole, {"로직": code}, efp)
|
||||
if got["갈래"] == WAIT:
|
||||
return None
|
||||
rows = got["줄비교"]
|
||||
out = {"줄": rows, "수": {k: sum(x["꼴"] == k for x in rows) for k in LOOKS}}
|
||||
out["못옮김"] = got["못옮김"]
|
||||
return out | ({"환산": got["환산"]} if got.get("환산") else {})
|
||||
|
||||
|
||||
def _wait(
|
||||
entry: dict, whole: Any, unit: str, why: str | None = None, at: tuple | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""고름 대기 — `at` = (맞춤셈, 블록, 줄 · 사용자 고름 없을 때) 이면 앞 셋에 줄비교."""
|
||||
keep = ("로직", "이름", "단위", "판정", "글")
|
||||
cands = [{k: c.get(k) for k in keep} for c in (entry.get("후보") or [])[: fit.TOP_OPT]]
|
||||
for c in cands:
|
||||
for i, c in enumerate(cands):
|
||||
c["계수"] = _need(str(c.get("로직") or ""), unit, whole)
|
||||
if at and i < COMPARE and c.get("로직"):
|
||||
c["줄비교"] = compare(*at[:3], whole, str(c["로직"]), *at[3:])
|
||||
if why is None:
|
||||
why = "맞는 기초 없음 — " + ((entry.get("판정") or {}).get("글") or "후보 없음")
|
||||
return {"갈래": WAIT, "까닭": why, "후보": cands}
|
||||
@@ -346,13 +405,14 @@ def plan(
|
||||
line: dict[str, Any],
|
||||
whole: Any,
|
||||
pick: dict | None = None,
|
||||
efp: dict | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 환산?, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김,
|
||||
놓음, 엑셀품, 후보?}` · `pick` = 사용자 `{로직?, 입력값?, 환산?, 잇기?}` — 로직 = 고른 기초(옵션 없으면
|
||||
거꾸로 찾음 · 늘 견줌) · 환산 = SK `환산` 꼴 · 잇기 = 못옮김 잇기(`_Plan.link` · 틀림 `LinkError`)."""
|
||||
entry, pick = entry or {}, pick or {}
|
||||
state = (entry.get("판정") or {}).get("상태")
|
||||
efp = finger.excel(block, line)
|
||||
efp = efp or finger.excel(block, line) # 줄비교 = 한 번 푼 지문
|
||||
unit = str(line.get("단위") or "").strip() or "식"
|
||||
if pick.get("로직"):
|
||||
code = str(pick["로직"])
|
||||
@@ -363,7 +423,7 @@ def plan(
|
||||
code = entry["차례"][0]
|
||||
given = ((entry.get("옵션") or {}).get(code) or {}).get("입력") or {}
|
||||
else:
|
||||
return _wait(entry, whole, unit)
|
||||
return _wait(entry, whole, unit, at=(entry, block, line, efp))
|
||||
doc = {"키": "SK-000000", "이름": "견줌", "단위": unit, "바탕": {"로직": code},
|
||||
"입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip
|
||||
if pick.get("환산"):
|
||||
@@ -386,7 +446,8 @@ def plan(
|
||||
if code not in [c.get("로직") for c in cands]:
|
||||
cands = [{"로직": code, "이름": row.get("이름")}, *cands]
|
||||
why = f"{UNIT_WHY} — {str(e).split(' · ', 1)[-1]}"
|
||||
return _wait({"후보": cands}, whole, unit, why)
|
||||
at = None if pick else (entry, block, line, efp)
|
||||
return _wait({"후보": cands}, whole, unit, why, at)
|
||||
doc["환산"] = {"계수": coef}
|
||||
tb = float(calc.ratio_of(doc, row, whole))
|
||||
doc["더한줄"] = extras(tb)
|
||||
@@ -396,11 +457,12 @@ def plan(
|
||||
base = {**dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], []), "놓음": []}
|
||||
return {**head, "갈래": BASE, **base}
|
||||
work = _Plan(doc, whole)
|
||||
work.prices = elem.prices(block)
|
||||
work.labor({t: x["품"] for t, x in efp["인력"].items() if x.get("품")})
|
||||
work.machines([m for m in efp["장비"] if m.get("시간")], line)
|
||||
work.materials([m for m in efp["자재"] if m.get("품")])
|
||||
work.link(pick.get("잇기") or [])
|
||||
out = {k: doc[k] for k in KEYS[:4]}
|
||||
changed = any(out.values()) or bool(work.lost)
|
||||
kept = {"못옮김": work.lost, "놓음": doc.get("놓음") or []}
|
||||
kept = {"못옮김": work.lost, "놓음": doc.get("놓음") or [], "줄비교": work.rows}
|
||||
return {**head, **out, "갈래": SK if changed else BASE, **kept}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
조합 알약 배(`M01_MasterData_Store_Combo`) · 엑셀 맞춤 배(`M02_Hopyo_Ours` · `_Match`) ·
|
||||
표 기준배(`master_basis`) · 계산서 줄 환산 근거(714-1). 서버 단독(③) — TS 는 서버가 준 글만 씀.
|
||||
- 단 SK 단위 바꿈 환산(`swap` · 728-1)은 짝(①) — `A00_Common/master_unit_swap.ts` · 거울 시험 `test_728_1_unit_swap.py`.
|
||||
- 개수 묶음(개 · EA · 본 · 매) = 1 : 1 같은 성격 · 조 · 세트는 묶음 밖(한 조 = 여럿).
|
||||
- 개수 묶음(개 · EA · 본 · 매 · 개소) = 1 : 1 같은 성격 · hr = 시간(별칭) · 조 · 세트는 묶음 밖(한 조 = 여럿).
|
||||
- 부피 ↔ 무게 = 단위중량 표 CC000337 에서 고른 줄(`unit_weight`)을 받을 때만 · 기본 줄 없음
|
||||
(사용자 확인 거리) · 밀도(kg ↔ ℓ) · ㎾h 등은 환산 없이 경고 글만.
|
||||
"""
|
||||
@@ -17,7 +17,16 @@ import unicodedata
|
||||
from decimal import Decimal
|
||||
|
||||
# 맞대기 꼴(`plain`) 별칭 — NFKC · 소문자 · 빈칸 없앤 뒤
|
||||
ALIAS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
|
||||
ALIAS = {
|
||||
"ea": "개",
|
||||
"ton": "톤",
|
||||
"t": "톤",
|
||||
"l": "ℓ",
|
||||
"리터": "ℓ",
|
||||
"set": "조",
|
||||
"세트": "조",
|
||||
"hr": "시간",
|
||||
}
|
||||
# 보이는 꼴(`shown`) 별칭 — 표 기준 · 결과단위 분모(원문 기호 ㎡ · ㎥ 그대로)
|
||||
SHOWN = {"ea": "개", "톤": "ton", "t": "ton", "㎞": "km", "hr": "시간", "min": "분", "㎏": "kg"}
|
||||
# 같은 성격 — {성격: {맞대기 단위: 기준 단위 몇 배}}
|
||||
@@ -26,7 +35,7 @@ UNITS = {
|
||||
"길이": {"mm": 1, "cm": 10, "m": 1000, "km": 1000000},
|
||||
"넓이": {"cm2": 1, "m2": 10000},
|
||||
"부피": {"cm3": 1, "ℓ": 1000, "m3": 1000000},
|
||||
"개수": {"개": 1, "본": 1, "매": 1},
|
||||
"개수": {"개": 1, "본": 1, "매": 1, "개소": 1},
|
||||
}
|
||||
_SCALE = {u: (kind, n) for kind, group in UNITS.items() for u, n in group.items()}
|
||||
WEIGHT_TABLE = "CC000337" # 건설품셈 공통 1-2-4 재료의 단위중량(㎏/㎥)
|
||||
|
||||
@@ -40,6 +40,9 @@ CASES = [
|
||||
["톤", "㎥", 1600, None],
|
||||
["10㎥", "kg", 2400, None],
|
||||
["조", "개", None, None],
|
||||
["hr", "시간", None, None], # 746-1 별칭
|
||||
["개소", "개", None, None],
|
||||
["10개소", "개", None, None],
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""746-1 지킴 — 불러오기 고름 대기 후보 블록 줄비교 · 못옮김 · 기계 줄 M01 요소 후보 · 단위 별칭(요구 585).
|
||||
|
||||
고름 대기 블록(북부청 임도 실물 엑셀 · 읽기만) = 판정 · 후보 차례 그대로 + 앞 셋 `줄비교`(같음 · 품다름 · 엑셀만 ·
|
||||
기초만 · 엑셀 품 = 엑셀 지문 품) · 그 기초로 고르면 만드는 SK 와 같은 셈(더한줄 · 뺀줄 수) · 요소 후보 셋 ·
|
||||
hr = 시간 · 개소 = 개(같은 성격). 쓰기 없음 · master_data 원본 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as sg
|
||||
from M02_MasterTemplete import M02_Hopyo_SkLink as sklink
|
||||
from M02_MasterTemplete import M02_Hopyo_SkPatch as skp
|
||||
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict
|
||||
from resources.tester import helper_m02_hopyo_golden as golden
|
||||
from resources.tester.test_m02_hopyo_excel import master_hash
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "resources/master_data/scripts"))
|
||||
import master_unit as mu # noqa: E402
|
||||
|
||||
FILE, BLOCK = "북부청임도", 38 # 고름 대기 · 1위와 견줌 되는 블록
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def wait():
|
||||
before = master_hash()
|
||||
if not ours.ready():
|
||||
ours.build()
|
||||
blocks, first = sg.calls(golden.read(FILE))
|
||||
block, line = blocks[BLOCK], first[BLOCK]
|
||||
entry = verdict.block(block, line, hm.targets())
|
||||
got = skp.plan(entry, block, line, m01.loaded()[1])
|
||||
yield {"entry": entry, "block": block, "line": line, "plan": got}
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_판정_후보_차례_그대로_앞_셋_줄비교(wait):
|
||||
got, entry = wait["plan"], wait["entry"]
|
||||
assert got["갈래"] == skp.WAIT
|
||||
assert [c["로직"] for c in got["후보"]] == [c["로직"] for c in entry["후보"][: len(got["후보"])]]
|
||||
assert all("줄비교" in c for c in got["후보"][: skp.COMPARE])
|
||||
assert all("줄비교" not in c for c in got["후보"][skp.COMPARE :])
|
||||
first = got["후보"][0]["줄비교"]
|
||||
assert first and first["줄"]
|
||||
assert sum(first["수"].values()) == len(first["줄"])
|
||||
assert {x["꼴"] for x in first["줄"]} <= set(skp.LOOKS)
|
||||
|
||||
|
||||
def test_줄비교_엑셀_품_지문_그대로_고르면_같은_셈(wait):
|
||||
first = wait["plan"]["후보"][0]
|
||||
rows = first["줄비교"]["줄"]
|
||||
efp = finger.excel(wait["block"], wait["line"])
|
||||
labor = {t: x["품"] for t, x in efp["인력"].items() if x.get("품")}
|
||||
for x in rows:
|
||||
if x["갈래"] == "인력":
|
||||
assert x["엑셀"] == pytest.approx(labor.get(x["이름"]) or None), x
|
||||
assert x["단위"] == "인"
|
||||
if x["꼴"] == "같음":
|
||||
assert x["엑셀"] == pytest.approx(x["기초"], rel=0.01), x
|
||||
if x["꼴"] == "엑셀만":
|
||||
assert x["엑셀"] and not x["기초"]
|
||||
if x["꼴"] == "기초만":
|
||||
assert x["기초"] and not x["엑셀"]
|
||||
made = skp.plan(
|
||||
wait["entry"], wait["block"], wait["line"], m01.loaded()[1], {"로직": first["로직"]}
|
||||
)
|
||||
gone = sum(1 for x in rows if x["꼴"] == "기초만" and x["갈래"] != "자재")
|
||||
assert len(made["뺀줄"]) == gone
|
||||
assert made["못옮김"] == first["줄비교"]["못옮김"]
|
||||
|
||||
|
||||
def test_요소_후보_셋():
|
||||
got = elem.labor("작업반장")
|
||||
assert 0 < len(got) <= elem.TOP and {x["이름"] for x in got[:2]} == {"작업반장"}
|
||||
assert all(x["키"].startswith("LB") and x["단가"] for x in got)
|
||||
got = elem.machines("굴착기 0.7㎥")
|
||||
assert got and all(x["키"].startswith("EQ") and "굴착기" in x["이름"] for x in got)
|
||||
got = elem.materials("시멘트", "kg")
|
||||
assert got and all(x["키"].startswith("MT") for x in got) and len(got) <= elem.TOP
|
||||
lost = {"갈래": "기계", "이름": "굴착기", "규격": "0.7"}
|
||||
assert elem.lost(lost)[0]["키"].startswith("EQ")
|
||||
|
||||
|
||||
def test_안_이은_기계_줄_EQ_후보():
|
||||
line = {"차례": 0, "이름": "굴착기(타이어)", "규격": "0.7㎥", "단위": "hr", "갈래": "기계"}
|
||||
got = sklink.left(line, None)
|
||||
assert got["까닭"] == sklink.WHY["기계"]
|
||||
assert got["후보"] and all(x["키"].startswith("EQ") for x in got["후보"])
|
||||
|
||||
|
||||
def test_단위_별칭_hr_시간_개소_개():
|
||||
assert mu.plain("hr") == mu.plain("시간") and mu.plain("HR") == "시간"
|
||||
assert mu.same_kind("개소", "개") and mu.swap("개소", "개")["배"] == 1
|
||||
assert mu.swap("10개소", "개")["배"] == 10 and mu.swap("hr", "시간")["계수"] is None
|
||||
assert mu.swap("조", "개소")["계수"] == "사용자" # 조는 묶음 밖 그대로
|
||||
Reference in New Issue
Block a user