fix(m02-df): 흐름 셈 = 옛 셈 — 빈 단가 줄을 부르는 덧줄은 빈 값 · 전수 1,472 모두 맞음
- 빈 단가(blank) 요소 값 = 「#N/A」(서버 Compute · 화면 값 받기 Entry_Detail · Api CfAnswer.blank) → 그 값을 쓰는 식은 빈 값 · 결과가 오류일 때만 끊긴 곳 「단가 없는 품목을 씀」(Eval NoPrice · Compute 같음) - 변환: 비목 합 · 덧줄의 비목 이름 = 빈 줄 0(SUM(IFERROR…)) · 호표 줄 = IF(수량=0,0,수량×단가) · 줄.이름은 빈 줄이면 빈 값(옛 env 줄) - 오류 값 칸은 NA() 로(Compute _is_err · Eval isErrorValue) - Compute 700줄 넘어 표 찾기 · 값 같음을 M02_DF_Lookup.py 로 나눔(Compute 가 다시 내보냄) - 전수 대조 시험입력 · 기본 입력 1,472/1,472 · 시험 test_m02_df_model 37 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
@@ -47,6 +47,8 @@ export interface CfAnswer {
|
||||
name?: string;
|
||||
unit?: string;
|
||||
reason?: string;
|
||||
/** 빈 단가 · 맞는 재료 없음 — 옛 셈처럼 그 값만 비우고 셈은 계속(흐름은 「#N/A」) */
|
||||
blank?: boolean;
|
||||
/** 재료 조건 찾기가 여럿 맞을 때 후보 다섯 */
|
||||
candidates?: CfMaterial[];
|
||||
}
|
||||
|
||||
@@ -28,6 +28,15 @@ from M01_MasterData import (
|
||||
) # _num — M01 Router 가 이 파일을 부름(서로 부름)
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Values as dv
|
||||
from M02_MasterTemplete.DataFlow.M02_DF_Lookup import ( # noqa: F401 — 시험이 cp._lookup 을 부름
|
||||
RANGE_RULE,
|
||||
_cond_of,
|
||||
_exact,
|
||||
_is_err,
|
||||
_lookup,
|
||||
_num_of,
|
||||
_same,
|
||||
)
|
||||
|
||||
_CELL = re.compile(r"^([A-Z]+)(\d+)$")
|
||||
TABLE_PORT, OUTPUT_PORT = "표", "값"
|
||||
@@ -156,7 +165,7 @@ def _element_value(
|
||||
got = dv.one_value(item, ctx["local"])
|
||||
if not got["ok"] and got.get("blank"): # 옛 셈처럼 그 값만 비움(알림) — 셈은 계속
|
||||
ctx["notes"].append(f"「{box.get('name')} · {el.get('name')}」 {got.get('reason')}")
|
||||
return None
|
||||
return NA_VALUE # 그 값을 쓰는 식은 빈 값(옛 엔진) — 비목 합은 0
|
||||
if not got["ok"]:
|
||||
errors.append(f"「{box.get('name')} · {el.get('name')}」 {got.get('reason')}")
|
||||
return None
|
||||
@@ -305,32 +314,6 @@ def design_inputs(lg: dict) -> list[dict]:
|
||||
|
||||
|
||||
# ── 선 규칙(`M02_DF_Rule.applyRule` 와 같은 뜻 · 값 기준) ────────────────────
|
||||
def _num_of(v: Any) -> Decimal | None:
|
||||
if isinstance(v, bool) or v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float, Decimal)):
|
||||
return Decimal(str(v))
|
||||
return None
|
||||
|
||||
|
||||
def _same(a: Any, b: Any) -> bool:
|
||||
"""엑셀 「=」 — 수끼리는 수로 · 글끼리는 대소문자 무시 · 빈 칸은 0 · 빈 글과 같음."""
|
||||
na, nb = _num_of(a), _num_of(b)
|
||||
if a is None or b is None:
|
||||
other = b if a is None else a
|
||||
return other is None or other == "" or _num_of(other) == 0
|
||||
if na is not None and nb is not None:
|
||||
return na == nb
|
||||
if isinstance(a, str) and isinstance(b, str):
|
||||
return a.lower() == b.lower()
|
||||
return False
|
||||
|
||||
|
||||
def _exact(a: Any, b: Any) -> bool:
|
||||
"""표 찾기 같음 — 글끼리는 대소문자까지(옛 find · Rule lookup FIND) · 그 밖은 엑셀 「=」."""
|
||||
return a == b if isinstance(a, str) and isinstance(b, str) else _same(a, b)
|
||||
|
||||
|
||||
def _compare(a: Any, op: str, b: Any) -> bool:
|
||||
if op in ("=", "≠"):
|
||||
return _same(a, b) == (op == "=")
|
||||
@@ -359,67 +342,6 @@ def _rounded(value: Any, r: dict) -> Any:
|
||||
)
|
||||
|
||||
|
||||
def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
"""표에서 찾기 — 같음 · 범위(아래 < 값 ≤ 위 · 빈 끝 열림 · 아래 = 위면 같음) · 맞은 줄 하나
|
||||
· 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌 · 여럿이면 범위 위 끝이 가장 작은 줄
|
||||
(옛 엔진 find 와 같음 · Rule lookup 과 같음)."""
|
||||
if not isinstance(block, list) or len(block) < 2:
|
||||
return None, "오류"
|
||||
head = [str(h if h is not None else "") for h in block[0]]
|
||||
out = rule.get("col")
|
||||
two = out not in head and {out + dv.RANGE_LOW, out + dv.RANGE_HIGH} <= set(head)
|
||||
if out not in head and not two:
|
||||
return None, "오류"
|
||||
tests, ups = [], []
|
||||
rk = head.index(RANGE_RULE) if RANGE_RULE in head else None
|
||||
for c in rule.get("conds") or []:
|
||||
col, want = str(c.get("col")), _cond_of(conds, c.get("from") or {})
|
||||
if col in head:
|
||||
i = head.index(col)
|
||||
tests.append(lambda row, i=i, want=want: row[i] is None or _exact(row[i], want))
|
||||
elif col + dv.RANGE_LOW in head and col + dv.RANGE_HIGH in head:
|
||||
lo, hi, x = (
|
||||
head.index(col + dv.RANGE_LOW),
|
||||
head.index(col + dv.RANGE_HIGH),
|
||||
_num_of(want),
|
||||
)
|
||||
ups.append(hi)
|
||||
|
||||
def fits(row, lo=lo, hi=hi, x=x) -> bool:
|
||||
low, high = _num_of(row[lo]), _num_of(row[hi])
|
||||
if low is not None and low == high: # 숫자 하나(아래 = 위) = 같음(브레인 정함)
|
||||
return x is not None and x == low
|
||||
word = str(row[rk] or "") if rk is not None else "" # 경계 — 이상 · 미만(Rule 같음)
|
||||
return (
|
||||
x is not None
|
||||
and (
|
||||
row[lo] is None
|
||||
or (low is not None and (low <= x if word[:2] == "이상" else low < x))
|
||||
)
|
||||
and (
|
||||
row[hi] is None
|
||||
or (high is not None and (x < high if word[-2:] == "미만" else x <= high))
|
||||
)
|
||||
)
|
||||
|
||||
tests.append(fits)
|
||||
else:
|
||||
return None, "오류"
|
||||
at = head.index(out + dv.RANGE_LOW if two else out) # 값 칸이 범위로 갈림 — 아래 = 위면 그 값
|
||||
rows = [list(r) + [None] * len(head) for r in block[1:]]
|
||||
hits = [r for r in rows if r[at] not in (None, "") and all(t(r) for t in tests)]
|
||||
if not hits:
|
||||
return None, "표에 없는 조건"
|
||||
for hi in ups: # 위 끝만 적힌 범위(「20 이하」 「40 이하」 …) — 위 끝이 가장 작은 줄
|
||||
tops = [_num_of(r[hi]) for r in hits if _num_of(r[hi]) is not None]
|
||||
hits = [r for r in hits if r[hi] is None or not tops or _num_of(r[hi]) == min(tops)]
|
||||
if len(hits) > 1:
|
||||
return None, "표에 여럿 맞음"
|
||||
if two and not _same(hits[0][at], hits[0][head.index(out + dv.RANGE_HIGH)]):
|
||||
return None, "오류"
|
||||
return hits[0][at], None
|
||||
|
||||
|
||||
def apply_rule(rule: dict, value: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
"""(값, 까닭) — 까닭 = 통과 안 됨 · 표에 없는 조건 · 표에 여럿 맞음 · 오류 · None."""
|
||||
kind = rule.get("kind")
|
||||
@@ -465,12 +387,6 @@ def _gate_conds(rule: dict) -> tuple[list[dict], str]:
|
||||
return rows, rule.get("join") or "그리고"
|
||||
|
||||
|
||||
def _cond_of(conds: dict, port: dict) -> Any:
|
||||
"""조건 알약 값 — 열쇠 「상자|알약」 이 먼저 · 없으면 알약 이름(Rule condOf)."""
|
||||
key = f"{port.get('box')}|{port.get('name')}"
|
||||
return conds[key] if key in conds else conds.get(port.get("name"))
|
||||
|
||||
|
||||
# ── 계산(`M02_DF_Eval.evalLogic` 와 같은 차례) ─────────────────────────────
|
||||
def _depends(lg: dict) -> list[tuple[str, str]]:
|
||||
"""상자 사이 기댐 — 선 + 선 규칙 조건 알약 + 재료 조건 찾기 알약(Model `depends`)."""
|
||||
@@ -516,7 +432,7 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict:
|
||||
if str(p.get("cell") or "").strip():
|
||||
port = p.get("port")
|
||||
v = arrived[port] if arrived.get(port) is not None else given.get(port)
|
||||
_place(cells, p["cell"], {"식": "NA()"} if v == NA_VALUE else v)
|
||||
_place(cells, p["cell"], {"식": "NA()"} if _is_err(v) else v) # 오류 값 · 빈 단가
|
||||
for v in func.get("variables") or []:
|
||||
if v.get("cell"):
|
||||
_place(cells, v["cell"], given.get(v.get("name"), v.get("example")))
|
||||
@@ -524,7 +440,6 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict:
|
||||
|
||||
|
||||
REST = "앞 조건이 안 맞아 쉬는 갈래"
|
||||
RANGE_RULE = "·범위규칙" # 표 블록 줄마다 범위 경계 규칙 칸(Values._table · Rule RANGE_RULE)
|
||||
NA_VALUE = "#N/A" # 함수 칸으로 가는 표 찾기 실패 — 칸에 NA() · 결과 오류일 때만 끊긴 곳(Eval 같음)
|
||||
|
||||
|
||||
@@ -545,6 +460,15 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
|
||||
src = ln.get("from") or {}
|
||||
v = ports.get(src.get("box"), {}).get(src.get("name"))
|
||||
rule = ln.get("rule")
|
||||
if (
|
||||
v == NA_VALUE
|
||||
and (boxes.get(src.get("box"), {}).get("input") or {}).get("mode") == "group"
|
||||
):
|
||||
to = ln["to"] # 빈 단가 — 결과가 오류일 때만 끊긴 곳(Eval NoPrice 와 같음)
|
||||
text = f"「{name(to.get('box'))}」 — 단가 없는 품목을 씀(「{src.get('name')}」)"
|
||||
brk = {"kind": "단가 없는 품목", "level": "문제", "box": to.get("box")}
|
||||
brk |= {"port": to.get("name"), "link": ln.get("id"), "text": text}
|
||||
late.setdefault(to.get("box"), []).append(brk)
|
||||
if not rule:
|
||||
return v
|
||||
if v is None and rule.get("kind") in ("scale", "round"):
|
||||
|
||||
@@ -284,6 +284,12 @@ class _Flow:
|
||||
return take({"box": self.logic(node), "name": "결과"}, f"{node[1]} 결과")
|
||||
if kind == "@port":
|
||||
return take(node[1], node[1]["name"])
|
||||
if kind == "@sum0": # 비목 합 — 빈 줄(빈 단가 · 못 셈 덧줄)은 0(옛 엔진 sums += 0)
|
||||
cells = [f"IFERROR({take(p, p['name'])},0)" for p in node[1]]
|
||||
return f"SUM({','.join(cells)})" if cells else "0"
|
||||
if kind == "@line": # 호표 줄 수량 × 단가 — 수량 0 이면 0(옛 엔진 · 빈 단가여도)
|
||||
q, p = self.excel(node[1], take, scope), self.excel(node[2], take, scope)
|
||||
return f"IF({q}=0,0,{q}*{p})"
|
||||
if kind == "@sum":
|
||||
cells = [take(p, p["name"]) for p in node[1]]
|
||||
return f"SUM({','.join(cells)})" if cells else "0"
|
||||
@@ -294,8 +300,8 @@ class _Flow:
|
||||
raise Unsupported(f"모르는 식 마디 「{kind}」")
|
||||
|
||||
def name(self, name: str, take, scope: dict) -> str:
|
||||
if name in scope: # 덧줄의 비목 합 · 줄 금액
|
||||
return self.excel(("@sum", scope[name]), take, scope)
|
||||
if name in scope: # 덧줄의 비목 합(빈 줄 = 0) · 줄 금액(빈 줄이면 빈 값 — 옛 env 「줄」)
|
||||
return self.excel(("@sum0" if name in COSTS else "@sum", scope[name]), take, scope)
|
||||
if name in self.named:
|
||||
return take(self.named[name], name)
|
||||
if name in self.specs:
|
||||
@@ -382,7 +388,7 @@ def _body(f: _Flow, row: dict) -> None:
|
||||
ref = item["요소"] # 그룹 = 키 앞 두 글자(「MO:경유:{지역}」 = 유가전력) · 없으면 줄 종류
|
||||
group = GROUP_OF.get(ref[:2], str(item.get("종류"))) if isinstance(ref, str) else "재료"
|
||||
price = f.priced(group, ref, unit, f"{name} 단가")
|
||||
part = f.func(f"{name} 금액", ("bin", "*", qty, price))
|
||||
part = f.func(f"{name} 금액", ("@line", qty, price))
|
||||
parts[item["비목"]].append(part)
|
||||
lines.setdefault(f"줄.{name}", []).append(part)
|
||||
for extra in row.get("덧줄") or []:
|
||||
@@ -392,7 +398,7 @@ def _body(f: _Flow, row: dict) -> None:
|
||||
parts[extra["비목"]].append(part)
|
||||
for c in COSTS:
|
||||
if parts[c]:
|
||||
_output(f, c, c, f.func(f"{c} 합", ("@sum", parts[c])), rule, "계")
|
||||
_output(f, c, c, f.func(f"{c} 합", ("@sum0", parts[c])), rule, "계")
|
||||
|
||||
|
||||
# ── 자동 배치 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -272,6 +272,7 @@ export async function fetchValues(
|
||||
await logicDone;
|
||||
for (const a of asks) {
|
||||
const got = answers.find((x) => x.id === a.q.id);
|
||||
if (got && !got.ok && got.blank) (values[a.box] ??= {})[a.port] = "#N/A"; // 빈 단가 — Eval 이 NA()
|
||||
if (!got?.ok) errors.push(`${a.port}${got?.reason ? ` — ${got.reason}` : ""}`);
|
||||
else (values[a.box] ??= {})[a.port] = got.rows ?? got.value ?? null;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface CfBreak {
|
||||
| "통과 안 됨"
|
||||
| "표에 없는 조건"
|
||||
| "맞는 재료 없음"
|
||||
| "단가 없는 품목"
|
||||
| "조건 겹침"
|
||||
| "조건 안 맞음"
|
||||
| "오류";
|
||||
@@ -112,6 +113,10 @@ const TEXT = {
|
||||
"「{from}」 → 「{box}」 선이 조건({rule})을 통과하지 않음",
|
||||
"Line 「{from}」 → 「{box}」 doesn't pass ({rule})",
|
||||
],
|
||||
NoPrice: [
|
||||
"「{box}」 — 단가 없는 품목을 씀(「{pill}」)",
|
||||
"「{box}」 — uses an item with no price (「{pill}」)",
|
||||
],
|
||||
NoRow: [
|
||||
"「{from}」 → 「{box}」 선 — 표에 맞는 줄이 없음",
|
||||
"Line 「{from}」 → 「{box}」 — no matching table row",
|
||||
@@ -234,7 +239,8 @@ function runFunc(
|
||||
const sheet: Sheet = structuredClone(func.sheet);
|
||||
for (const i of func.inputs) {
|
||||
const v = arrive[i.port] ?? given[i.port] ?? null;
|
||||
if (v === NA_VALUE) sheet.칸[i.cell.toUpperCase()] = { 식: "NA()" };
|
||||
if (isErrorValue(v))
|
||||
sheet.칸[i.cell.toUpperCase()] = { 식: "NA()" }; // 오류 값 · 빈 단가
|
||||
else put(sheet.칸, i.cell, v);
|
||||
}
|
||||
for (const v of func.variables)
|
||||
@@ -275,6 +281,19 @@ export function evalLogic(
|
||||
/** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */
|
||||
const through = (l: CfLink): CfValue => {
|
||||
const v = valueOf(l.from.box, l.from.name);
|
||||
// 빈 단가(#N/A) — 옛 엔진처럼 그 값을 쓰는 식은 빈 값 · 결과가 오류일 때만 끊긴 곳(late)
|
||||
if (v === NA_VALUE && lg.boxes.find((b) => b.id === l.from.box)?.input?.mode === "group") {
|
||||
const text = tl("NoPrice", { box: name(l.to.box), pill: l.from.name });
|
||||
const brk: CfBreak = {
|
||||
kind: "단가 없는 품목",
|
||||
level: "문제",
|
||||
box: l.to.box,
|
||||
port: l.to.name,
|
||||
link: l.id,
|
||||
text,
|
||||
};
|
||||
late.set(l.to.box, [...(late.get(l.to.box) ?? []), brk]);
|
||||
}
|
||||
if (!l.rule) return v;
|
||||
if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null;
|
||||
const conds = Object.fromEntries(
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""M02 흐름도 — 선 규칙 「표에서 찾기」 · 값 같음(Compute 에서 나눔 · Rule lookup 과 같은 뜻).
|
||||
|
||||
표 블록 = 머리 줄 + 줄 · 범위 칸 「이름·아래」 「이름·위」 · 끝 칸 「·범위규칙」.
|
||||
빈 조건 칸 = 무엇이든 · 꺼낼 칸 빈 줄은 뺌 · 글은 대소문자까지 · 여럿이면 위 끝 최소 줄(옛 find).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Values as dv
|
||||
|
||||
RANGE_RULE = "·범위규칙" # 표 블록 줄마다 범위 경계 규칙 칸(Values._table · Rule RANGE_RULE)
|
||||
|
||||
|
||||
def _num_of(v: Any) -> Decimal | None:
|
||||
if isinstance(v, bool) or v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float, Decimal)):
|
||||
return Decimal(str(v))
|
||||
return None
|
||||
|
||||
|
||||
def _same(a: Any, b: Any) -> bool:
|
||||
"""엑셀 「=」 — 수끼리는 수로 · 글끼리는 대소문자 무시 · 빈 칸은 0 · 빈 글과 같음."""
|
||||
na, nb = _num_of(a), _num_of(b)
|
||||
if a is None or b is None:
|
||||
other = b if a is None else a
|
||||
return other is None or other == "" or _num_of(other) == 0
|
||||
if na is not None and nb is not None:
|
||||
return na == nb
|
||||
if isinstance(a, str) and isinstance(b, str):
|
||||
return a.lower() == b.lower()
|
||||
return False
|
||||
|
||||
|
||||
def _is_err(v: Any) -> bool:
|
||||
return isinstance(v, str) and re.fullmatch(r"#[A-Z/0!?]+", v) is not None
|
||||
|
||||
|
||||
def _exact(a: Any, b: Any) -> bool:
|
||||
"""표 찾기 같음 — 글끼리는 대소문자까지(옛 find · Rule lookup FIND) · 그 밖은 엑셀 「=」."""
|
||||
return a == b if isinstance(a, str) and isinstance(b, str) else _same(a, b)
|
||||
|
||||
|
||||
def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
"""표에서 찾기 — 같음 · 범위(아래 < 값 ≤ 위 · 빈 끝 열림 · 아래 = 위면 같음) · 맞은 줄 하나
|
||||
· 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌 · 여럿이면 범위 위 끝이 가장 작은 줄
|
||||
(옛 엔진 find 와 같음 · Rule lookup 과 같음)."""
|
||||
if not isinstance(block, list) or len(block) < 2:
|
||||
return None, "오류"
|
||||
head = [str(h if h is not None else "") for h in block[0]]
|
||||
out = rule.get("col")
|
||||
two = out not in head and {out + dv.RANGE_LOW, out + dv.RANGE_HIGH} <= set(head)
|
||||
if out not in head and not two:
|
||||
return None, "오류"
|
||||
tests, ups = [], []
|
||||
rk = head.index(RANGE_RULE) if RANGE_RULE in head else None
|
||||
for c in rule.get("conds") or []:
|
||||
col, want = str(c.get("col")), _cond_of(conds, c.get("from") or {})
|
||||
if col in head:
|
||||
i = head.index(col)
|
||||
tests.append(lambda row, i=i, want=want: row[i] is None or _exact(row[i], want))
|
||||
elif col + dv.RANGE_LOW in head and col + dv.RANGE_HIGH in head:
|
||||
lo, hi, x = (
|
||||
head.index(col + dv.RANGE_LOW),
|
||||
head.index(col + dv.RANGE_HIGH),
|
||||
_num_of(want),
|
||||
)
|
||||
ups.append(hi)
|
||||
|
||||
def fits(row, lo=lo, hi=hi, x=x) -> bool:
|
||||
low, high = _num_of(row[lo]), _num_of(row[hi])
|
||||
if low is not None and low == high: # 숫자 하나(아래 = 위) = 같음(브레인 정함)
|
||||
return x is not None and x == low
|
||||
word = str(row[rk] or "") if rk is not None else "" # 경계 — 이상 · 미만(Rule 같음)
|
||||
return (
|
||||
x is not None
|
||||
and (
|
||||
row[lo] is None
|
||||
or (low is not None and (low <= x if word[:2] == "이상" else low < x))
|
||||
)
|
||||
and (
|
||||
row[hi] is None
|
||||
or (high is not None and (x < high if word[-2:] == "미만" else x <= high))
|
||||
)
|
||||
)
|
||||
|
||||
tests.append(fits)
|
||||
else:
|
||||
return None, "오류"
|
||||
at = head.index(out + dv.RANGE_LOW if two else out) # 값 칸이 범위로 갈림 — 아래 = 위면 그 값
|
||||
rows = [list(r) + [None] * len(head) for r in block[1:]]
|
||||
hits = [r for r in rows if r[at] not in (None, "") and all(t(r) for t in tests)]
|
||||
if not hits:
|
||||
return None, "표에 없는 조건"
|
||||
for hi in ups: # 위 끝만 적힌 범위(「20 이하」 「40 이하」 …) — 위 끝이 가장 작은 줄
|
||||
tops = [_num_of(r[hi]) for r in hits if _num_of(r[hi]) is not None]
|
||||
hits = [r for r in hits if r[hi] is None or not tops or _num_of(r[hi]) == min(tops)]
|
||||
if len(hits) > 1:
|
||||
return None, "표에 여럿 맞음"
|
||||
if two and not _same(hits[0][at], hits[0][head.index(out + dv.RANGE_HIGH)]):
|
||||
return None, "오류"
|
||||
return hits[0][at], None
|
||||
|
||||
|
||||
def _cond_of(conds: dict, port: dict) -> Any:
|
||||
"""조건 알약 값 — 열쇠 「상자|알약」 이 먼저 · 없으면 알약 이름(Rule condOf)."""
|
||||
key = f"{port.get('box')}|{port.get('name')}"
|
||||
return conds[key] if key in conds else conds.get(port.get("name"))
|
||||
@@ -968,3 +968,69 @@ def test_표_찾기_글은_대소문자까지_같아야() -> None:
|
||||
)
|
||||
assert got == [[3, None], [1, None], [0.45, None], [0.96, None], [None, "표에 없는 조건"]]
|
||||
assert [list(cp._lookup(rule, block, {"g": g})) for g in gs] == got
|
||||
|
||||
|
||||
def _fn(i: str, ins: list[str], formula: str) -> dict:
|
||||
cells = {f"B{k + 2}": {"값": 0} for k in range(len(ins))}
|
||||
out = f"B{len(ins) + 2}"
|
||||
cells[out] = {"식": formula}
|
||||
func = {"sheet": {"id": f"s{i}", "이름": i, "칸": cells}, "variables": []}
|
||||
func["inputs"] = [{"port": p, "cell": f"B{k + 2}"} for k, p in enumerate(ins)]
|
||||
func["output"] = {"cell": out, "name": i, "unit": "-"}
|
||||
return {
|
||||
"id": i,
|
||||
"kind": "function",
|
||||
"name": i,
|
||||
"color": "#000",
|
||||
"unit": "-",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"func": func,
|
||||
}
|
||||
|
||||
|
||||
def test_빈_단가_쓰는_식은_빈_값_비목_합은_0_끊긴_곳(monkeypatch) -> None:
|
||||
g = {"mode": "group", "group": "재료", "variables": [], "elements": [
|
||||
{"ref": "MT1", "name": "p1", "spec": "", "unit": "-"}, {"ref": "MT2", "name": "p2", "spec": "", "unit": "-"}]} # fmt: skip
|
||||
boxes = [
|
||||
{"id": "g", "kind": "input", "name": "재료", "color": "#000", "unit": "-", "x": 0, "y": 0, "input": g},
|
||||
_fn("a", ["p1"], "IF(2=0,0,2*B2)"),
|
||||
_fn("b", ["p2"], "IF(1=0,0,1*B2)"),
|
||||
_fn("z", ["a", "b"], "(B2+B3)*2/100"), # 줄 이름을 부르는 덧줄 — 빈 줄이 끼면 빈 값
|
||||
_fn("s", ["a", "b", "z"], "SUM(IFERROR(B2,0),IFERROR(B3,0),IFERROR(B4,0))"),
|
||||
{"id": "o", "kind": "output", "name": "재료비", "color": "#000", "unit": "원", "x": 0, "y": 0,
|
||||
"output": {"cost": "재료비", "unit": "원"}},
|
||||
] # fmt: skip
|
||||
pairs = [
|
||||
("g", "p1", "a", "p1"),
|
||||
("g", "p2", "b", "p2"),
|
||||
("a", "a", "z", "a"),
|
||||
("b", "b", "z", "b"),
|
||||
]
|
||||
pairs += [
|
||||
("a", "a", "s", "a"),
|
||||
("b", "b", "s", "b"),
|
||||
("z", "z", "s", "z"),
|
||||
("s", "s", "o", "값"),
|
||||
]
|
||||
links = [{"id": f"l{k}", "from": {"box": a, "name": ap}, "to": {"box": b, "name": bp}}
|
||||
for k, (a, ap, b, bp) in enumerate(pairs)] # fmt: skip
|
||||
lg = {
|
||||
"key": None,
|
||||
"name": "t",
|
||||
"resultUnit": "",
|
||||
"boxes": boxes,
|
||||
"links": links,
|
||||
"userElements": [],
|
||||
}
|
||||
values = {"g": {"p1": "#N/A", "p2": 100}}
|
||||
got = _run(
|
||||
f"const ev = E.evalLogic({json.dumps(lg, ensure_ascii=False)}, {json.dumps(values)});\n"
|
||||
"out([ev.cost.재료비, ev.ports.z.z, ev.breaks.map(b => [b.kind, b.box])]);"
|
||||
)
|
||||
assert got == [100, "#N/A", [["단가 없는 품목", "a"]]]
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp
|
||||
|
||||
srv = cp.run(lg, values, lambda key: {}, [])
|
||||
assert float(srv["cost"]["재료비"]) == 100 and srv["ports"]["z"]["z"] == "#N/A"
|
||||
assert [[b["kind"], b["box"]] for b in srv["breaks"]] == got[2]
|
||||
|
||||
Reference in New Issue
Block a user