Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -258,6 +258,7 @@ def combine(
|
||||
groups: dict[str, list] = {}
|
||||
for p in parts:
|
||||
groups.setdefault(str(p.get("절") or p.get("제목") or ""), []).append(p)
|
||||
single = bool(primary) and len(groups) == 1 # 절 하나 = 덧은 후보만(같은 몫 이중 계상 막음)
|
||||
have = {primary} if primary else set()
|
||||
|
||||
def machine(E: dict, where: str) -> None:
|
||||
@@ -297,6 +298,9 @@ def combine(
|
||||
ok = mixed or reached(c, line, p.get("제목"), p, prior, got)
|
||||
chosen.append(one(c, p.get("제목"), f, x, "몫" if ok else "후보", g))
|
||||
have.add(c)
|
||||
if single:
|
||||
return [c if c["단계"] in ("1순위", "후보") else {**c, "단계": "후보"} for c in chosen
|
||||
if c["단계"] != "장비 몫"] # fmt: skip
|
||||
return chosen
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ COMBO = (0.85, 0.75, 0.6)
|
||||
COVER = 0.6
|
||||
SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9)
|
||||
EVERY = 20 # 블록 몇 개마다 캐시 덮음
|
||||
CALC_VERSION = 2 # 맞춤셈 꼴 · 셈이 바뀌면 올림 — 옛 캐시 안 씀
|
||||
CALC_VERSION = 3 # 맞춤셈 꼴 · 셈이 바뀌면 올림 — 옛 캐시 안 씀
|
||||
COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"}
|
||||
HOUR = {"hr", "시간", "h"}
|
||||
NO_NUMBER = "숫자 판정 없음"
|
||||
|
||||
@@ -73,9 +73,11 @@ def _target(code: str, line: dict, ctx: Ctx, found: dict | None, use: bool, **ex
|
||||
one = ctx.names.get(code) or {"이름": "", "단위": ""}
|
||||
vals, unread = hm.spec_inputs(line, hm._logic_specs(code))
|
||||
src = {k: "규격" for k in vals}
|
||||
use = use or (found or {}).get("수준", 9) <= 2 # 맞춘 옵션 입력 = 품 비교와 같은 입력
|
||||
typed = ((found or {}).get("식아님") or {}).get("칸") or {} # 엑셀이 값으로 넣은 「0 = 식」 칸
|
||||
for k, v in ((found or {}).get("입력") or {}).items() if use else ():
|
||||
if str(vals.get(k)) != str(v):
|
||||
vals[k], src[k] = v, "블록"
|
||||
vals[k], src[k] = v, "엑셀 값 · 식 아님" if k in typed else "블록"
|
||||
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
|
||||
share = extra.pop("몫", None)
|
||||
times = conv["배"] * share if conv["배"] is not None and share else conv["배"]
|
||||
@@ -109,11 +111,17 @@ def _pair(pair: dict, line: dict, ctx: Ctx) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _qty(entry: dict, found: dict | None) -> dict:
|
||||
def _qty(entry: dict, found: dict | None, share: float | None = None) -> dict:
|
||||
"""품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용) · 같은 갈래 여러 줄은 합쳐 견줌
|
||||
(화면 품 비교 열쇠 = 갈래 가나다 차례 + 로 묶음)."""
|
||||
(화면 품 비교 열쇠 = 갈래 가나다 차례 + 로 묶음) · 몫 = 대상 몫(섞인 줄 10% = 우리 × 0.1)."""
|
||||
ex = entry.get("품") or {"인력": {}, "장비": [], "자재": []}
|
||||
us = (found or {}).get("우리품") or option.ours_qty(None)
|
||||
if share:
|
||||
k = lambda q: round(q * share, 6) # noqa: E731
|
||||
us = {"인력": {t: k(q) for t, q in us["인력"].items()}} | {
|
||||
"장비": [{**x, "시간": k(x["시간"])} for x in us["장비"]],
|
||||
"자재": [{**x, "수량": k(x["수량"])} for x in us["자재"]],
|
||||
}
|
||||
same = {}
|
||||
for t, a in ex["인력"].items():
|
||||
b = us["인력"].get(t)
|
||||
@@ -159,6 +167,7 @@ def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict:
|
||||
found = (entry.get("옵션") or {}).get(top)
|
||||
use = state in ("맞음", "다른 점")
|
||||
combo = _picked(entry)
|
||||
share = None
|
||||
if combo:
|
||||
targets = [
|
||||
_target(
|
||||
@@ -176,11 +185,13 @@ def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict:
|
||||
for c in entry.get("조합") or []:
|
||||
if not c["체크"]:
|
||||
tags.append(f"기계 몫 {c['로직']} — 시간 = 엑셀 값(우리 셈 아님)")
|
||||
if any("블록" in t["출처"].values() for t in targets):
|
||||
if any({"블록", "엑셀 값 · 식 아님"} & set(t["출처"].values()) for t in targets):
|
||||
tags.append("옵션 — 블록에서")
|
||||
tags += [f"근거 {b} {n}" for b, n in (entry.get("지문") or {}).get("출처") or []]
|
||||
out = {"상태": state, "체크": _check(entry, line, ctx), "대상": targets}
|
||||
out.update(후보=entry.get("후보") or [], 글=judged["글"], 꼬리표=tags, 품=_qty(entry, found))
|
||||
out.update(
|
||||
후보=entry.get("후보") or [], 글=judged["글"], 꼬리표=tags, 품=_qty(entry, found, share)
|
||||
)
|
||||
if found and found.get("갈리는칸"):
|
||||
out["갈리는칸"] = found["갈리는칸"]
|
||||
return out
|
||||
|
||||
@@ -151,6 +151,8 @@ def spec_inputs(line: dict[str, Any], specs: list[dict[str, Any]]) -> tuple[dict
|
||||
words = [(w, letter) for letter, ws in LETTERS.items() for w in ws if w in name]
|
||||
letter = max(words, key=lambda x: len(x[0]))[1] if words else None
|
||||
got: Any = None
|
||||
if letter == "L" and "거리" in name and named.get("L", (0, None))[1] in ("cm", "mm"):
|
||||
letter = None # 「L=35cm」 = 부재 길이 — 운반 · 거리(m) 칸 아님
|
||||
if letter in named:
|
||||
value = _json(_in_unit(*named[letter], spec.get("단위")))
|
||||
got = value if not picks or value in picks else None
|
||||
@@ -160,7 +162,15 @@ def spec_inputs(line: dict[str, Any], specs: list[dict[str, Any]]) -> tuple[dict
|
||||
label = str(labels.get(opt) or opt)
|
||||
for text in (label, _PAREN.sub("", label)):
|
||||
flat = _spec_text(text)
|
||||
if len(flat) > 1 and (flat in pool or (unit and flat == f"{unit}당")):
|
||||
if (
|
||||
_NUM.fullmatch(flat) and len(flat) > 1
|
||||
): # 수 고르기 = 제 단위가 붙은(또는 맨) 수만
|
||||
hits += (
|
||||
[(len(flat), opt)]
|
||||
if any(_num_in(flat, w, spec) for w in tokens)
|
||||
else []
|
||||
)
|
||||
elif len(flat) > 1 and (flat in pool or (unit and flat == f"{unit}당")):
|
||||
hits.append((len(flat), opt))
|
||||
# 규격 낱말이 고르기 글 머리(「4회」 → 「4회사용시」)
|
||||
hits += [(len(w), opt) for w in tokens if len(w) > 1 and flat.startswith(w)]
|
||||
@@ -172,6 +182,19 @@ def spec_inputs(line: dict[str, Any], specs: list[dict[str, Any]]) -> tuple[dict
|
||||
return values, unread
|
||||
|
||||
|
||||
_NUM = re.compile(r"\d+(?:\.\d+)?")
|
||||
|
||||
|
||||
def _num_in(flat: str, pool: str, spec: dict[str, Any]) -> bool:
|
||||
"""규격 글에 수 `flat` 이 따로 서 있고 뒤에 다른 단위가 안 붙음(「35cm」 의 35 ≠ 경사 35°)."""
|
||||
own = _spec_text(spec.get("단위"))
|
||||
for m in re.finditer(rf"(?<![\d.]){re.escape(flat)}(?![\d.])", pool):
|
||||
after = re.match(r"[a-z㎜㎝㎞㎡㎥°%]+", pool[m.end() :])
|
||||
if not after or (own and after.group().startswith(own)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ── 환산 ─────────────────────────────────────────────────────────────
|
||||
def factor(have: Any, want: Any, spec: Any = "") -> dict[str, Any]:
|
||||
"""`{배, 갈래}` — 우리 수량 = 엑셀 수량 × 배 · 배 None = 사용자 입력."""
|
||||
@@ -180,8 +203,10 @@ def factor(have: Any, want: Any, spec: Any = "") -> dict[str, Any]:
|
||||
if len(units) > 1:
|
||||
ok = plain in {mm.plain_unit(u) for u in units}
|
||||
return {"배": 1 if ok else None, "갈래": "두 단위" if ok else "입력"}
|
||||
if plain == mm.plain_unit(want):
|
||||
if plain.replace("/", "") == mm.plain_unit(want).replace("/", ""): # 공/㎥ ≡ 공㎥
|
||||
return {"배": 1, "갈래": "같음"}
|
||||
if str(want or "").strip() == "자재": # 결과단위 「원/자재 단위」 = 줄 단위 그대로
|
||||
return {"배": 1, "갈래": "자재 단위"}
|
||||
a, b = mp.kind_of(have), mp.kind_of(want)
|
||||
if a is not None and a == b:
|
||||
scale = mp.UNITS[a]
|
||||
|
||||
@@ -87,7 +87,7 @@ def excel_numbers(texts: list[str], spec: dict[str, Any] | None = None) -> set[D
|
||||
if vs:
|
||||
alt = "|".join(re.escape(x) for x in vs)
|
||||
pats.append(
|
||||
re.compile(r"(?<![\d,.])" + num + r"\s*(?:" + alt + r")(?![A-Za-z가-힣])", re.I)
|
||||
re.compile(r"(?<![\d,.])" + num + r"\s*(?:" + alt + r")(?![A-Za-z가-힣\d])", re.I)
|
||||
)
|
||||
pats.append(
|
||||
re.compile(re.escape(nfkc(spec["이름"])) + r"\s*[:=(]?\s*" + num + r"(?![\d,])")
|
||||
@@ -121,6 +121,7 @@ def space(code: str, ex: dict[str, Any] | None = None) -> list[tuple[str, list,
|
||||
row = whole.logic(code)
|
||||
auto = m01.mau.values(whole, row)
|
||||
finds, _ = _uses(row)
|
||||
guard = m01.mau.guards(row)
|
||||
ex = ex or {}
|
||||
texts = ex.get("글") or []
|
||||
coef = {k.lower(): Decimal(str(v)) for k, v in (ex.get("계수") or {}).items()}
|
||||
@@ -159,6 +160,10 @@ def space(code: str, ex: dict[str, Any] | None = None) -> list[tuple[str, list,
|
||||
vals.add(Decimal(0))
|
||||
if name == "품":
|
||||
vals |= {v * times for v in ex.get("품값", set())}
|
||||
if (
|
||||
name in guard
|
||||
): # 「0 = 기본」 칸 — 옛 자리표 1 도 후보(엑셀이 그 칸으로 안 나눈 블록)
|
||||
vals.add(Decimal(1))
|
||||
vals.discard(None)
|
||||
out.append((name, sorted(vals, key=lambda v: (v != base, v)) or [base], "수"))
|
||||
return out
|
||||
@@ -205,7 +210,7 @@ def combos(
|
||||
return _product(sp2), "좁힘"
|
||||
base = {n: o[0] for n, o, _ in sp2}
|
||||
out = [dict(base)] + [{**base, n: v} for n, o, _ in sp2 for v in o[1:]]
|
||||
small = [(n, o) for n, o, k in sp2 if k != "수"]
|
||||
small = [(n, o) for n, o, k in sp2 if k != "수" or len(o) <= 4] # 작은 수 칸도 둘씩 짝
|
||||
for (n1, o1), (n2, o2) in itertools.combinations(small, 2):
|
||||
if len(o1) * len(o2) <= 400:
|
||||
out += [{**base, n1: a, n2: b} for a in o1 for b in o2]
|
||||
@@ -442,6 +447,37 @@ def _plain(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
_BY_FORMULA = re.compile(r"만약\(\s*(\w+)\s*>\s*0\s*,\s*\1\s*,(.*)\)\s*$")
|
||||
|
||||
|
||||
def by_formula(code: str, g: dict, ex: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""엑셀이 값으로 넣은 「0 = 식」 칸(운반회수 n 등) → {칸, 입력, 우리품}: 그 칸 0(원문 식) ·
|
||||
단위 칸 = 엑셀 글에 단위 붙어 하나만 있는 수(운반거리 6m · 경사 35°) · 없으면 그대로."""
|
||||
row = ours.whole().logic(code)
|
||||
steps = [m.groups() for s in row.get("중간") or [] if (m := _BY_FORMULA.match(s["식"]))]
|
||||
hit = {n: g[n] for n, rest in steps if re.search(r"[*/]", rest) and _num(g.get(n)) > 0}
|
||||
if not hit:
|
||||
return None
|
||||
tried = {**g, **{n: Decimal(0) for n in hit}}
|
||||
for s in row.get("입력") or []:
|
||||
n, picks = s["이름"], s.get("고르기") or []
|
||||
if n in hit or not s.get("단위"):
|
||||
continue
|
||||
nums = sorted(excel_numbers(ex.get("글") or [], s))
|
||||
nums = [p for p in picks if _num(p) in nums] if picks else nums
|
||||
if len(nums) == 1:
|
||||
tried[n] = nums[0]
|
||||
one = {"칸": {n: _plain(x) for n, x in hit.items()}, "우리품": ours_qty(ours.run(code, tried))}
|
||||
return one | {"입력": {n: _plain(x) for n, x in tried.items()}}
|
||||
|
||||
|
||||
def _num(v: Any) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(v))
|
||||
except ArithmeticError:
|
||||
return Decimal(0)
|
||||
|
||||
|
||||
def find(code: str, efp: dict[str, Any], text: list[str] | None = None) -> dict[str, Any]:
|
||||
"""로직 하나의 옵션 거꾸로 찾기 → {수준, 판정, 입력, 갈리는칸, 견줌, 우리품, 거리, 섞기전}."""
|
||||
none = {"수준": 9, "판정": "없음", "입력": {}, "갈리는칸": [], "견줌": None}
|
||||
@@ -482,15 +518,20 @@ def find(code: str, efp: dict[str, Any], text: list[str] | None = None) -> dict[
|
||||
auto = m01.mau.values(ours.whole(), ours.whole().logic(code)) # 움직인 칸 + 자동값과 다른 칸
|
||||
base = compare(efp, v, fit.TOL[2], base=True) or {}
|
||||
labor = {k for k, x in efp["인력"].items() if x.get("품")}
|
||||
miss = lvl == 9 and round(d, 4) >= 1.0 # 못 맞춤(e배 넘게 어긋남) — 우리 품 · 입력을 내지 않음
|
||||
return {
|
||||
"수준": lvl,
|
||||
"판정": verdict,
|
||||
"입력": {n: _plain(x) for n, x in g.items() if n in act or str(x) != str(auto.get(n))},
|
||||
"입력": {}
|
||||
if miss
|
||||
else {n: _plain(x) for n, x in g.items() if n in act or str(x) != str(auto.get(n))},
|
||||
"모든입력": {n: _plain(x) for n, x in g.items()},
|
||||
"갈리는칸": split,
|
||||
"견줌": compare(efp, v, fit.TOL[2]),
|
||||
"직종같음": bool(v["L"]) and set(k for k, q in v["L"].items() if q) == labor,
|
||||
"우리품": ours_qty(v),
|
||||
"우리품": ours_qty(None if miss else v),
|
||||
"못맞춤": miss,
|
||||
"식아님": None if miss else by_formula(code, g, ex), # 엑셀 값 칸 · 원문 식이면
|
||||
"거리": round(d, 4),
|
||||
"섞기전": lvl == 9 and bool(base.get("핵심")),
|
||||
"어떻게": how,
|
||||
|
||||
@@ -33,7 +33,7 @@ from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, specnums
|
||||
|
||||
mb, mu = m01.cm.mf.mb, m01.cm.mf.mp.mm.mu # 묶음(712-1) · 단위(712-2) 한 벌
|
||||
FP_VERSION = 1 # 지문 꼴이 바뀌면 올림 — 옛 캐시 버림
|
||||
FP_VERSION = 2 # 지문 꼴이 바뀌면 올림 — 옛 캐시 버림
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
_SUB = re.compile(r"^\s*로직\(\s*(\w+)\s*(?:,(.*))?\)\s*$")
|
||||
EQ_CODE = re.compile(r"^\d{4}-\d{4}$")
|
||||
|
||||
@@ -21,7 +21,7 @@ from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
|
||||
_num = money_fmt.fmt # 꼬리표 수 — 공용 수량 꼴(1만 넘어도 지수 꼴 아님)
|
||||
OUTSIDE = "참고 후보 — 근거 조항 아님"
|
||||
@@ -68,6 +68,8 @@ def _diffs(efp: dict, found: dict) -> list[str]:
|
||||
a, b = x.get("품"), ours_l.get(t)
|
||||
if a and b and not option.near(a, b, fit.TOL[2]):
|
||||
tags.append(f"{t} 엑셀 {_num(a)} · 우리 {_num(b)}")
|
||||
elif a and not b and found.get("못맞춤"):
|
||||
tags.append(f"{t} 엑셀 {_num(a)} → –(우리 셈 조건 못 찾음)")
|
||||
elif a and not b:
|
||||
tags.append(f"{t} 엑셀만 {_num(a)}")
|
||||
tags += [f"{t} 우리만 {_num(q)}" for t, q in ours_l.items() if t not in efp["인력"]]
|
||||
@@ -80,6 +82,20 @@ def _diffs(efp: dict, found: dict) -> list[str]:
|
||||
return tags
|
||||
|
||||
|
||||
def formula_tag(code: str, found: dict | None) -> str | None:
|
||||
"""「운반회수 42 = 엑셀 값(식 아님) · 원문 식이면 보통인부 0.17 · 30,077원(운반거리 6 · …)」."""
|
||||
f = (found or {}).get("식아님")
|
||||
if not f:
|
||||
return None
|
||||
cells = " · ".join(f"{k} {_num(v)}" for k, v in f["칸"].items())
|
||||
qty = " · ".join(f"{t} {_num(q)}" for t, q in f["우리품"]["인력"].items()) or "–"
|
||||
won = (labor_doc.run_line(option.ours.whole(), code, f["입력"]).get("기준") or {}).get("계")
|
||||
won_text = f" · {money_fmt.fmt(won)}원" if won is not None else ""
|
||||
mine = {**f["칸"], **((found or {}).get("입력") or {})} # 맞춘 칸 뺀 원문 식 조건
|
||||
used = " · ".join(f"{k} {_num(v)}" for k, v in f["입력"].items() if k not in mine)
|
||||
return f"{cells} = 엑셀 값(식 아님) · 원문 식이면 {qty}{won_text}({used})"
|
||||
|
||||
|
||||
def _state(efp: dict, code: str, found: dict | None, theirs: list) -> str:
|
||||
lvl = 9 if (found is None or fit.single(efp)) else found["수준"]
|
||||
if lvl <= 1:
|
||||
@@ -173,7 +189,7 @@ def judge(efp: dict, top: list[str], opts: dict, theirs: list, got: dict) -> dic
|
||||
t1, found = top[0], opts.get(top[0]) or {}
|
||||
state = _state(efp, t1, opts.get(t1), theirs)
|
||||
exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"]
|
||||
tags = []
|
||||
tags = [t for t in [formula_tag(t1, found)] if t and state in ("맞음", "다른 점")]
|
||||
if state == "맞음":
|
||||
label = _label(t1, found.get("입력") or {})
|
||||
text = " · ".join(x for x in ("맞음 — " + t1, names.get(t1, ""), label) if x)
|
||||
@@ -183,7 +199,7 @@ def judge(efp: dict, top: list[str], opts: dict, theirs: list, got: dict) -> dic
|
||||
tags.append(f"{' · '.join(found['갈리는칸'])} 못 가름")
|
||||
return {"상태": state, "글": text, "꼬리표": tags}
|
||||
if state == "다른 점":
|
||||
tags = _diffs(efp, found)
|
||||
tags = _diffs(efp, found) + tags
|
||||
return {"상태": state, "글": " · ".join(["다른 점 — " + t1, *tags[:2]]), "꼬리표": tags}
|
||||
return {"상태": "후보", "글": f"후보 {len(top[: fit.TOP_OPT])}", "꼬리표": []}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
"""견본 입력 — 마스터 화면 보기용 자동값(프로젝트 기본값 아님 · 저장 안 함).
|
||||
|
||||
고르기 = 목록 첫째 · 범위 = 최솟값 · 찾기 조건 값으로 쓰이는 수 = 그 표 그 칸 가장 낮은 줄 값
|
||||
(범위 줄이면 그 줄에 드는 아래 끝) · 나누는 자리에 오는 수 = 1 · 그 밖 0.
|
||||
(범위 줄이면 그 줄에 드는 아래 끝) · 나누는 자리에 오는 수 = 1 · 그 밖 0
|
||||
(「만약(X > 0, X, 기본)」 의 X 는 나누는 자리여도 0 = 기본).
|
||||
찾기·나누기 쓰임은 로직 식(중간 · 결과 · 호표 수량·요소 · 덧줄)을 풀어 알아냄.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
import master_formula as mf
|
||||
@@ -46,12 +48,21 @@ def _uses(row: dict) -> tuple[dict, set]:
|
||||
for cond, arg in part[2]:
|
||||
if arg[0] == "name":
|
||||
finds.setdefault(arg[1], []).append((part[1], cond))
|
||||
guard = guards(row) # 「0 = 기본」 칸은 나누는 자리로 안 퍼뜨림(자동값 0 = 기본)
|
||||
for step in reversed(row.get("중간", [])):
|
||||
if step["이름"] in divs:
|
||||
divs |= _names(mf.parse(step["식"]))
|
||||
divs |= _names(mf.parse(step["식"])) - guard
|
||||
return finds, divs
|
||||
|
||||
|
||||
_GUARD = re.compile(r"만약\(\s*(\w+)\s*>\s*0\s*,\s*\1\s*,")
|
||||
|
||||
|
||||
def guards(row: dict) -> set:
|
||||
"""「만약(X > 0, X, 기본)」 꼴 중간 식의 X — 0 이면 원문 기본(25㎏ · 450분 · 식 · 표)."""
|
||||
return {m for s in row.get("중간", []) for m in _GUARD.findall(s["식"])}
|
||||
|
||||
|
||||
def _fits(master: mf.Master, ref: str, cond: str) -> list:
|
||||
"""표 `ref` 조건 칸 `cond` 에 드는 수 — 낮은 차례 · 범위 칸이면 줄마다 그 줄에 드는 끝
|
||||
(아래 끝이 열린 줄 「60 미만」 은 0)."""
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""784-1(요구 635 · 636 · 637) — 「우리 품」 잘못 보임 뿌리 고침 · 같은 흠 전수(느림 · 3~6 분).
|
||||
|
||||
자동값 「0 = 기본」 가드 칸 0 · 단산10 야면석소운반(GC001140) 우리 품 0.547619 · 대상 하나 · 배 1 ·
|
||||
미리 셈 282,681 = 엑셀 · 못 맞춤(수준 9 · 거리 ≥ 1) = 우리 품 · 입력 안 냄 ·
|
||||
여덟 파일 1위 품 비교 10배 넘는 줄 = 알려진 예외만 ·
|
||||
「자재 단위」 · 공/㎥ 배 1 · 봉화 단산91 단끊기 · 골든 지킴.
|
||||
읽기만 · 서버 없음 · master_data 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Line as hl
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from resources.tester import helper_m02_hopyo_golden as golden
|
||||
|
||||
FILES = (
|
||||
"북부청임도",
|
||||
"북부청사방",
|
||||
"거창",
|
||||
"봉화",
|
||||
"동부청영월",
|
||||
"울진대흥",
|
||||
"영덕계류",
|
||||
"울진소광",
|
||||
)
|
||||
KNOWN_10X = {"GC000110"} # 엑셀 롤러 10.4 시간 읽기(로직 아님)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def _ours_ready():
|
||||
if not ours.ready():
|
||||
ours.build()
|
||||
|
||||
|
||||
def _ctx(tag: str):
|
||||
got = hm.targets()
|
||||
|
||||
class C:
|
||||
pass
|
||||
|
||||
ctx = C()
|
||||
ctx.got, ctx.names = got, {x["로직"]: x for x in got["로직"]}
|
||||
ctx.mats = {x["자재"]: x for x in got["자재"]}
|
||||
ctx.units = {int(h["번"]): h for h in golden.read(tag)["호표"]}
|
||||
return ctx
|
||||
|
||||
|
||||
def _unit(tag: str, no: int) -> tuple[dict, dict]:
|
||||
for _, block, line in fit.units(golden.read(tag)):
|
||||
if block and "_지문" not in block and block.get("번") == no:
|
||||
return block, line
|
||||
raise AssertionError(f"{tag} 단산{no} 셈 단위 없음")
|
||||
|
||||
|
||||
def _auto(code: str) -> dict:
|
||||
whole = ours.whole()
|
||||
return m01.mau.values(whole, whole.logic(code))
|
||||
|
||||
|
||||
def test_1_가드_칸_자동값_0():
|
||||
a = _auto("GC001140")
|
||||
assert a["회당운반량"] == a["실작업시간"] == a["운반회수"] == 0
|
||||
assert a["운반거리"] == 1 # 가드 아닌 나눗수 칸은 1 그대로
|
||||
b = _auto("GF000156")
|
||||
assert b["모래단위중량"] == b["자갈단위중량"] == 0
|
||||
|
||||
|
||||
def test_2_단산10_옵션():
|
||||
block, line = _unit("북부청임도", 10)
|
||||
found = option.find("GC001140", fit_efp(block, line), block.get("글"))
|
||||
assert found["우리품"]["인력"] == {"보통인부": 0.547619}
|
||||
assert found["입력"]["운반무게"] == 575 and found["입력"]["운반회수"] == 42
|
||||
assert found["식아님"]["칸"] == {"운반회수": 42} # D — 엑셀 값(식 아님)
|
||||
|
||||
|
||||
def fit_efp(block: dict, line: dict) -> dict:
|
||||
return verdict.finger.excel(block, line)
|
||||
|
||||
|
||||
def test_3_못_맞춤_우리품_안_냄():
|
||||
block, line = _unit("북부청임도", 12)
|
||||
found = option.find("GF000113", fit_efp(block, line), block.get("글"))
|
||||
assert found["수준"] == 9 and found["거리"] >= 1.0
|
||||
assert found["못맞춤"] and found["입력"] == {} and not found["우리품"]["인력"]
|
||||
tags = verdict._diffs(fit_efp(block, line), found)
|
||||
assert any("우리 셈 조건 못 찾음" in t for t in tags)
|
||||
|
||||
|
||||
def _money(line: dict, targets: list[dict]) -> float | None:
|
||||
rows = [
|
||||
{"로직": t["로직"], "별칭": None, "메모": None, "입력값": t["입력값"]}
|
||||
| {"수량": {"손": (line.get("수량") or 0) * t["배"]}}
|
||||
for t in targets
|
||||
if t.get("로직") and t.get("배") is not None
|
||||
]
|
||||
if not rows:
|
||||
return None
|
||||
doc = labor.clean({"종류": "일위대가", "열": "", "단위": None, "담은로직": rows, "자재": []})
|
||||
return (labor.calc(doc, {}).get("반영합") or {}).get("계")
|
||||
|
||||
|
||||
def test_4_확인_창_줄_단산10():
|
||||
book = golden.read("북부청임도")
|
||||
blocks = {int(b["번"]): b for b in book["단산"]}
|
||||
h2 = next(h for h in book["호표"] if int(h["번"]) == 2)
|
||||
line = next(x for x in h2["줄"] if (x.get("참조") or {}).get("번") == 10)
|
||||
m = hl._numeric(line, _ctx("북부청임도"), verdict.block(blocks[10], line, hm.targets()))
|
||||
assert [t["로직"] for t in m["대상"]] == ["GC001140"] and m["대상"][0]["배"] == 1
|
||||
assert m["대상"][0]["출처"]["운반회수"] == "엑셀 값 · 식 아님"
|
||||
assert m["품"]["우리"]["인력"] == {"보통인부": 0.547619} and m["품"]["같음"]["보통인부"]
|
||||
assert _money(line, m["대상"]) == 282681 == line["금액"]["계"]
|
||||
|
||||
|
||||
def _ratios(ex: dict, us: dict) -> list[float]:
|
||||
out = [us["인력"][t] / a for t, a in ex["인력"].items() if a and us["인력"].get(t)]
|
||||
sums: dict[str, list[float]] = {}
|
||||
for x in ex["장비"]:
|
||||
sums.setdefault("+".join(sorted(x["갈래"])), [0.0, 0.0])[0] += x["시간"]
|
||||
for x in us["장비"]:
|
||||
if (one := sums.get("+".join(sorted(x["갈래"])))) is not None:
|
||||
one[1] += x["시간"]
|
||||
return out + [b / a for a, b in sums.values() if a and b]
|
||||
|
||||
|
||||
def test_5_여덟_파일_전수():
|
||||
far, leak, double = set(), [], []
|
||||
for tag in FILES:
|
||||
ctx = _ctx(tag)
|
||||
for key, block, line in fit.units(golden.read(tag)):
|
||||
entry = verdict.block(block, line, ctx.got)
|
||||
if not entry.get("차례") or key.startswith("호표:"):
|
||||
continue
|
||||
top = entry["차례"][0]
|
||||
found = (entry.get("옵션") or {}).get(top) or {}
|
||||
m = hl._numeric(line, ctx, entry)
|
||||
if any(r > 10 or r < 0.1 for r in _ratios(m["품"]["엑셀"], m["품"]["우리"])):
|
||||
far.add(top)
|
||||
if m["상태"] == "다른 점" and found.get("못맞춤"):
|
||||
leak += [key for t in m["대상"] if "블록" in t["출처"].values()]
|
||||
parts = (entry["지문"].get("몫") if "몫" in entry["지문"] else None) or []
|
||||
groups = {str(p.get("절") or p.get("제목") or "") for p in parts}
|
||||
if len(groups) == 1 and len([c for c in entry.get("조합") or [] if c["체크"]]) > 1:
|
||||
double.append(key)
|
||||
assert far <= KNOWN_10X, far
|
||||
assert not leak and not double
|
||||
|
||||
|
||||
def test_6_배_자재_단위_표기():
|
||||
assert hm.factor("㎡", "자재") == {"배": 1, "갈래": "자재 단위"}
|
||||
assert hm.factor("공/㎥", "공㎥")["배"] == 1
|
||||
assert hm.factor("m", "본")["배"] is None # 단위 환산 필요한 줄은 그대로 사용자 몫
|
||||
|
||||
|
||||
def test_6_규격_읽기_거리_cm_안_넣음():
|
||||
vals, _ = hm.spec_inputs(
|
||||
{"이름": "야면석소운반", "규격": "L=35cm", "단위": "㎡"}, hm._logic_specs("GC001140")
|
||||
)
|
||||
assert "운반거리" not in vals and "경사각도" not in vals
|
||||
vals, _ = hm.spec_inputs(
|
||||
{"이름": "x", "규격": "25mm", "단위": "㎥"}, hm._logic_specs("GF000156")
|
||||
)
|
||||
assert vals.get("골재최대치수") == Decimal(25) or vals.get("골재최대치수") == 25
|
||||
|
||||
|
||||
def test_7_봉화_단산91_단끊기():
|
||||
block, line = _unit("봉화", 91)
|
||||
entry = verdict.block(block, line, hm.targets())
|
||||
assert entry["차례"][0] == "GF000298" and entry["판정"]["상태"] == "맞음"
|
||||
|
||||
|
||||
def test_7_골든_지킴():
|
||||
res = golden.evaluate()
|
||||
people, new = golden.metrics(res, {"사람"}), golden.metrics(res, {"자동", "씨앗", "자동+씨앗"})
|
||||
assert people["1순위"] + new["1순위"] >= 163
|
||||
assert people["다른 점 틀림"] + new["다른 점 틀림"] <= 3
|
||||
Reference in New Issue
Block a user