Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_8

This commit is contained in:
2026-11-02 21:25:15 +09:00
8 changed files with 329 additions and 16 deletions
+2 -1
View File
@@ -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)) {
+125
View File
@@ -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 []
+6 -2
View File
@@ -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}
+72 -10
View File
@@ -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}
@@ -108,3 +108,8 @@
2. 불러오기 도중 자료 → 내말은 일위대가 불러오기 엑셀자료 자체를 보관할 필요가 없으며 필요한 경우 다시 불러와서 사용해야한다는 것임.
3. 미리 고름 → 「다른 점」 줄만
4. 강관비계 되살림 → 결국 M02의 데이터 중 시스템관리자인 내가 삭제하고 수정할 수 있음. 그러니 삭제된 내용이라면 그냥둬.
## 요구 585 (11-02 · 사용자 · 아이디어 검토)
또하나 일위대가 가져오기의 매칭과 관련하여 아이디어 검토
- 내가 생각했을때의 경쟁사 프로그램으로 출력한 엑셀파일에서 우리가 일위대가를 단순히 하나의 로직 그룹으로 보고 접근하게 문제가 되지 않나 생각되서 말이야. 우리의 기초로직은 M01에서 요소들과 단가산출 기초로 된걸 일위대가의 항목 하나하나와 비교하면 더 찾기 쉬운게 아닌가라는 생각이 듬.
+12 -3
View File
@@ -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 재료의 단위중량(㎏/㎥)
+3
View File
@@ -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("조", "개소")["계수"] == "사용자" # 조는 묶음 밖 그대로