feat(m02-df): 36-2 덧 — 재료 조건 찾기 「검색어」 틀 · 표 범위 경계 규칙 칸
- Model CfMaterialRule field 「검색어」 + template(「레미콘 {a}-{b}-{c}」) + slots(자리마다 알약) · materialSources · depends · 흐름 따라가기에 자리 알약도
- Compute 재료 조건 찾기 검색어 = 틀에 자리 알약 값을 끼운 글(옛 fill)을 dv.slot_price 값으로 넘김(서버 찾기는 sub4)
- 표 찾기: 표 블록 「·범위규칙」 칸(줄마다 초과·이하 · 이상·이하 · 이상·미만 · 이하)을 Rule lookup · Compute _lookup 이 따름 · 없으면 초과·이하
- 변환: 재료 검색어 자리 = 검색어 틀 · scope 구분 · 상세구분 · 조건 찾기 요소로 들어가는 선은 안 만듦(depends 로 이음)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
@@ -28,7 +28,10 @@ export interface CfQuery {
|
|||||||
unit?: string;
|
unit?: string;
|
||||||
field?: string;
|
field?: string;
|
||||||
inputs?: Record<string, unknown>;
|
inputs?: Record<string, unknown>;
|
||||||
slot?: Partial<CfElementRef> & { field?: "규격" | "이름" | "키"; op?: "포함" | "같음" };
|
slot?: Partial<CfElementRef> & {
|
||||||
|
field?: "규격" | "이름" | "키" | "검색어";
|
||||||
|
op?: "포함" | "같음";
|
||||||
|
};
|
||||||
value?: unknown;
|
value?: unknown;
|
||||||
pick?: string;
|
pick?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,10 @@ def _element_value(
|
|||||||
"field": rule.get("field"),
|
"field": rule.get("field"),
|
||||||
"op": rule.get("op"),
|
"op": rule.get("op"),
|
||||||
}
|
}
|
||||||
got = dv.slot_price(slot, ports.get(src.get("box"), {}).get(src.get("name")))
|
value = ports.get(src.get("box"), {}).get(src.get("name"))
|
||||||
|
if rule.get("field") == "검색어": # 틀의 자리마다 알약 값 → 채운 검색어 글(빈 자리면 None)
|
||||||
|
value, slot["unit"] = _fill_template(rule, ports), el.get("unit")
|
||||||
|
got = dv.slot_price(slot, value)
|
||||||
if not got["ok"] and el.get("default"):
|
if not got["ok"] and el.get("default"):
|
||||||
errors.append(
|
errors.append(
|
||||||
f"「{box.get('name')} · {el.get('name')}」 {got['reason']} — 기본 품목으로 셈"
|
f"「{box.get('name')} · {el.get('name')}」 {got['reason']} — 기본 품목으로 셈"
|
||||||
@@ -153,6 +156,20 @@ def _element_value(
|
|||||||
return got.get("value")
|
return got.get("value")
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_template(rule: dict, ports: dict) -> str | None:
|
||||||
|
"""재료 검색어 틀의 자리에 slots 알약 값 — 옛 엔진 fill 그대로(빈 자리 = None)."""
|
||||||
|
env = {}
|
||||||
|
for x in rule.get("slots") or []:
|
||||||
|
src = x.get("from") or {}
|
||||||
|
env[x.get("name")] = ports.get(src.get("box"), {}).get(src.get("name"))
|
||||||
|
if any(v is None for v in env.values()):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return store.cm.mf.fill(str(rule.get("template") or ""), env)
|
||||||
|
except store.cm.mf.FormulaError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _port(el: dict) -> str:
|
def _port(el: dict) -> str:
|
||||||
"""요소 알약 이름 — Model `elementPort`(고정 아니면 자리 이름 slot · 없으면 name)."""
|
"""요소 알약 이름 — Model `elementPort`(고정 아니면 자리 이름 slot · 없으면 name)."""
|
||||||
slot = str(el.get("slot") or "").strip()
|
slot = str(el.get("slot") or "").strip()
|
||||||
@@ -342,6 +359,7 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
|||||||
if out not in head and not two:
|
if out not in head and not two:
|
||||||
return None, "오류"
|
return None, "오류"
|
||||||
tests, ups = [], []
|
tests, ups = [], []
|
||||||
|
rk = head.index(RANGE_RULE) if RANGE_RULE in head else None
|
||||||
for c in rule.get("conds") or []:
|
for c in rule.get("conds") or []:
|
||||||
col, want = str(c.get("col")), _cond_of(conds, c.get("from") or {})
|
col, want = str(c.get("col")), _cond_of(conds, c.get("from") or {})
|
||||||
if col in head:
|
if col in head:
|
||||||
@@ -359,10 +377,17 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
|||||||
low, high = _num_of(row[lo]), _num_of(row[hi])
|
low, high = _num_of(row[lo]), _num_of(row[hi])
|
||||||
if low is not None and low == high: # 숫자 하나(아래 = 위) = 같음(브레인 정함)
|
if low is not None and low == high: # 숫자 하나(아래 = 위) = 같음(브레인 정함)
|
||||||
return x is not None and x == low
|
return x is not None and x == low
|
||||||
|
word = str(row[rk] or "") if rk is not None else "" # 경계 — 이상 · 미만(Rule 같음)
|
||||||
return (
|
return (
|
||||||
x is not None
|
x is not None
|
||||||
and (row[lo] is None or (low is not None and low < x))
|
and (
|
||||||
and (row[hi] is None or (high is not None and x <= high))
|
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)
|
tests.append(fits)
|
||||||
@@ -444,7 +469,12 @@ def _depends(lg: dict) -> list[tuple[str, str]]:
|
|||||||
for b in lg.get("boxes") or []:
|
for b in lg.get("boxes") or []:
|
||||||
for el in (b.get("input") or {}).get("elements") or []:
|
for el in (b.get("input") or {}).get("elements") or []:
|
||||||
if el.get("mode") == "rule" and el.get("rule"):
|
if el.get("mode") == "rule" and el.get("rule"):
|
||||||
out.append(((el["rule"].get("from") or {}).get("box"), b["id"]))
|
rule = el["rule"]
|
||||||
|
srcs = [
|
||||||
|
rule.get("from") or {},
|
||||||
|
*[x.get("from") or {} for x in rule.get("slots") or []],
|
||||||
|
]
|
||||||
|
out += [(p.get("box"), b["id"]) for p in srcs]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -482,6 +512,7 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
REST = "앞 조건이 안 맞아 쉬는 갈래"
|
REST = "앞 조건이 안 맞아 쉬는 갈래"
|
||||||
|
RANGE_RULE = "·범위규칙" # 표 블록 줄마다 범위 경계 규칙 칸(Values._table · Rule RANGE_RULE)
|
||||||
NA_VALUE = "#N/A" # 함수 칸으로 가는 표 찾기 실패 — 칸에 NA() · 결과 오류일 때만 끊긴 곳(Eval 같음)
|
NA_VALUE = "#N/A" # 함수 칸으로 가는 표 찾기 실패 — 칸에 NA() · 결과 오류일 때만 끊긴 곳(Eval 같음)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -103,10 +103,10 @@ class _Flow:
|
|||||||
|
|
||||||
# ── 바깥 값 알약 ──────────────────────────────────────────────────
|
# ── 바깥 값 알약 ──────────────────────────────────────────────────
|
||||||
def element(
|
def element(
|
||||||
self, group: str, ref: str, unit: str, label: str, rule: dict | None = None
|
self, group: str, ref: str, unit: str, label: str, rule: dict | None = None, **more: Any
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""그룹 입력 상자의 요소 알약 — 그룹마다 상자 하나 · 같은 ref · 단위 · 조건이면 알약 하나
|
"""그룹 입력 상자의 요소 알약 — 그룹마다 상자 하나 · 같은 ref · 단위 · 조건이면 알약 하나
|
||||||
· rule = 조건 찾기(field 「키」 — ref 의 {…} 에 알약 값을 끼운 요소 · CfMaterialRule)."""
|
· rule = 조건 찾기(CfMaterialRule · 알약 값은 Model depends 로 · 선 없음) · more = scope."""
|
||||||
key = f"el|{group}|{ref}|{unit}|{json.dumps(rule, ensure_ascii=False, sort_keys=True)}"
|
key = f"el|{group}|{ref}|{unit}|{json.dumps(rule, ensure_ascii=False, sort_keys=True)}"
|
||||||
if key in self.cache:
|
if key in self.cache:
|
||||||
return self.cache[key]
|
return self.cache[key]
|
||||||
@@ -120,8 +120,7 @@ class _Flow:
|
|||||||
name += "·"
|
name += "·"
|
||||||
one = {"ref": ref, "name": name, "spec": "", "unit": unit or "-", "mode": "fixed"}
|
one = {"ref": ref, "name": name, "spec": "", "unit": unit or "-", "mode": "fixed"}
|
||||||
if rule:
|
if rule:
|
||||||
one |= {"mode": "rule", "rule": rule, "slot": name}
|
one |= {"mode": "rule", "rule": rule, "slot": name, **more}
|
||||||
self.link(rule["from"], {"box": self.cache[gkey]["id"], "name": name})
|
|
||||||
els.append(one)
|
els.append(one)
|
||||||
self.cache[key] = {"box": self.cache[gkey]["id"], "name": name}
|
self.cache[key] = {"box": self.cache[gkey]["id"], "name": name}
|
||||||
return self.cache[key]
|
return self.cache[key]
|
||||||
@@ -139,14 +138,20 @@ class _Flow:
|
|||||||
if len(texts) > 1:
|
if len(texts) > 1:
|
||||||
raise Unsupported(f"자리 표시가 여러 칸에 낀 요소 「{ref}」")
|
raise Unsupported(f"자리 표시가 여러 칸에 낀 요소 「{ref}」")
|
||||||
((at, text),) = texts.items()
|
((at, text),) = texts.items()
|
||||||
|
if at == "검색어": # 재료 검색어 틀 — 자리마다 알약 · 서버는 옛 검색어 찾기(정함 · sub4)
|
||||||
|
names = list(dict.fromkeys(_HOLE.findall(text)))
|
||||||
|
slots = [{"name": n, "from": self.hole(n)} for n in names]
|
||||||
|
rule = {"field": "검색어", "op": "같음", "from": slots[0]["from"]}
|
||||||
|
rule |= {"template": text, "slots": slots}
|
||||||
|
scope = {k: str(v) for k, v in ref.items() if k in ("구분", "상세구분") and v}
|
||||||
|
tpl = json.dumps(plain(ref), ensure_ascii=False)
|
||||||
|
return ("@port", self.element(group, tpl, unit, f"{label} 자리", rule, scope=scope))
|
||||||
# 찾는 값 = 원문번호(「EQ:{기계}」 → 기계 · 「MO:경유:{지역}」 → "경유:"&지역)
|
# 찾는 값 = 원문번호(「EQ:{기계}」 → 기계 · 「MO:경유:{지역}」 → "경유:"&지역)
|
||||||
want = text.partition(":")[2] if at is None and ":" in text else text
|
want = text.partition(":")[2] if at is None and ":" in text else text
|
||||||
if _HOLE.fullmatch(want):
|
if _HOLE.fullmatch(want):
|
||||||
src = self.hole(want[1:-1])
|
src = self.hole(want[1:-1])
|
||||||
else:
|
else:
|
||||||
src = self.func(f"{label} 찾는 말", ("@text", want))
|
src = self.func(f"{label} 찾는 말", ("@text", want))
|
||||||
if at is not None:
|
|
||||||
self.notes.append(f"재료 검색어에 입력이 낌 「{text}」 — 조건 찾기(키 같음)로 옮김")
|
|
||||||
tpl = text if at is None else json.dumps(plain(ref), ensure_ascii=False)
|
tpl = text if at is None else json.dumps(plain(ref), ensure_ascii=False)
|
||||||
tpl += f"#{field}" if field else ""
|
tpl += f"#{field}" if field else ""
|
||||||
rule = {"field": "키", "op": "같음", "from": src}
|
rule = {"field": "키", "op": "같음", "from": src}
|
||||||
@@ -383,6 +388,11 @@ def _layout(f: _Flow) -> None:
|
|||||||
for ln in f.links:
|
for ln in f.links:
|
||||||
srcs = [ln["from"], *[c["from"] for c in (ln.get("rule") or {}).get("conds", [])]]
|
srcs = [ln["from"], *[c["from"] for c in (ln.get("rule") or {}).get("conds", [])]]
|
||||||
feeds.setdefault(ln["to"]["box"], set()).update(s["box"] for s in srcs)
|
feeds.setdefault(ln["to"]["box"], set()).update(s["box"] for s in srcs)
|
||||||
|
for bid, b in f.boxes.items(): # 조건 찾기 요소 — 값을 주는 알약 상자 뒤
|
||||||
|
for e in (b.get("input") or {}).get("elements") or []:
|
||||||
|
rule = e.get("rule") or {}
|
||||||
|
srcs = [rule.get("from"), *[x["from"] for x in rule.get("slots") or []]]
|
||||||
|
feeds.setdefault(bid, set()).update(p["box"] for p in srcs if p)
|
||||||
depth: dict[str, int] = {}
|
depth: dict[str, int] = {}
|
||||||
|
|
||||||
def level(bid: str, path: frozenset = frozenset()) -> int:
|
def level(bid: str, path: frozenset = frozenset()) -> int:
|
||||||
|
|||||||
@@ -27,9 +27,19 @@ export interface CfScope {
|
|||||||
* field 「키」 = 요소 ref 의 자리 표시 {…} 에 알약 값을 끼운 요소(op 같음) — ref 꼴 「EQ:{기계}」 · 「EQ:{기계}#가격」 ·
|
* field 「키」 = 요소 ref 의 자리 표시 {…} 에 알약 값을 끼운 요소(op 같음) — ref 꼴 「EQ:{기계}」 · 「EQ:{기계}#가격」 ·
|
||||||
* 「MO:경유:{지역}」 · 「LB:{직종}」 · 재료 검색어 묶음 JSON(검색어 안 {…}) · 옛 엔진 `fill` → `Master.get` 과 같은 뜻 */
|
* 「MO:경유:{지역}」 · 「LB:{직종}」 · 재료 검색어 묶음 JSON(검색어 안 {…}) · 옛 엔진 `fill` → `Master.get` 과 같은 뜻 */
|
||||||
export interface CfMaterialRule {
|
export interface CfMaterialRule {
|
||||||
field: "규격" | "이름" | "키";
|
field: "규격" | "이름" | "키" | "검색어";
|
||||||
op: "포함" | "같음";
|
op: "포함" | "같음";
|
||||||
from: CfPort;
|
from: CfPort;
|
||||||
|
/** field 「검색어」 — 옛 재료 검색어 찾기(master_pick.search) 의 틀(「레미콘 {a}-{b}-{c}」) · scope 구분 · 상세구분 안 */
|
||||||
|
template?: string;
|
||||||
|
/** 틀의 자리 {이름} 마다 값을 줄 알약 — from = 첫 자리 */
|
||||||
|
slots?: { name: string; from: CfPort }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 재료 조건 찾기가 값을 받는 알약 — from + 틀 자리들(겹침 없이) */
|
||||||
|
export function materialSources(rule: CfMaterialRule): CfPort[] {
|
||||||
|
const all = [rule.from, ...(rule.slots ?? []).map((x) => x.from)];
|
||||||
|
return all.filter((p, i) => all.findIndex((q) => q.box === p.box && q.name === p.name) === i);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 입력 상자가 고른 요소 — ref = 마스터 키 또는 사용자 요소 id
|
/** 입력 상자가 고른 요소 — ref = 마스터 키 또는 사용자 요소 id
|
||||||
@@ -446,7 +456,8 @@ export function depends(lg: CfLogic): { from: string; to: string }[] {
|
|||||||
}
|
}
|
||||||
for (const b of lg.boxes)
|
for (const b of lg.boxes)
|
||||||
for (const e of b.input?.elements ?? [])
|
for (const e of b.input?.elements ?? [])
|
||||||
if (e.mode === "rule" && e.rule) out.push({ from: e.rule.from.box, to: b.id });
|
if (e.mode === "rule" && e.rule)
|
||||||
|
for (const p of materialSources(e.rule)) out.push({ from: p.box, to: b.id });
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +595,8 @@ export function traceBack(lg: CfLogic, port: CfPort): CfStep[] {
|
|||||||
lg.links.find((l) => l.to.box === box.id && l.to.name === q.name),
|
lg.links.find((l) => l.to.box === box.id && l.to.name === q.name),
|
||||||
);
|
);
|
||||||
for (const e of box.input?.elements ?? [])
|
for (const e of box.input?.elements ?? [])
|
||||||
if (e.mode === "rule" && e.rule && elementPort(e) === p.name) visit(e.rule.from, "out");
|
if (e.mode === "rule" && e.rule && elementPort(e) === p.name)
|
||||||
|
for (const q of materialSources(e.rule)) visit(q, "out");
|
||||||
};
|
};
|
||||||
const box = boxOf(lg, port.box);
|
const box = boxOf(lg, port.box);
|
||||||
if (!box) return [];
|
if (!box) return [];
|
||||||
@@ -630,7 +642,11 @@ export function traceForward(lg: CfLogic, port: CfPort): CfStep[] {
|
|||||||
visit(l.to, "in", l);
|
visit(l.to, "in", l);
|
||||||
for (const b of lg.boxes)
|
for (const b of lg.boxes)
|
||||||
for (const e of b.input?.elements ?? [])
|
for (const e of b.input?.elements ?? [])
|
||||||
if (e.mode === "rule" && e.rule?.from.box === p.box && e.rule.from.name === p.name)
|
if (
|
||||||
|
e.mode === "rule" &&
|
||||||
|
e.rule &&
|
||||||
|
materialSources(e.rule).some((q) => q.box === p.box && q.name === p.name)
|
||||||
|
)
|
||||||
visit({ box: b.id, name: elementPort(e) }, "out");
|
visit({ box: b.id, name: elementPort(e) }, "out");
|
||||||
};
|
};
|
||||||
const box = boxOf(lg, port.box);
|
const box = boxOf(lg, port.box);
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ export function applyRule(
|
|||||||
return { value: got, why: null, formula };
|
return { value: got, why: null, formula };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 표 블록 범위 경계 규칙 칸(줄마다 「초과·이하」 「이상·이하」 「이상·미만」 「이하」 · Values._table) */
|
||||||
|
export const RANGE_RULE = "·범위규칙";
|
||||||
|
|
||||||
/** 표에서 찾기 — 줄마다 맞음 칸 · 맞은 줄 수 · INDEX/MATCH(범위 칸 「이름·아래」 「이름·위」 · 초과 · 이하)
|
/** 표에서 찾기 — 줄마다 맞음 칸 · 맞은 줄 수 · INDEX/MATCH(범위 칸 「이름·아래」 「이름·위」 · 초과 · 이하)
|
||||||
* 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌(옛 엔진 find · 서버 Compute `_lookup` 과 같음) */
|
* 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌(옛 엔진 find · 서버 Compute `_lookup` 과 같음) */
|
||||||
function lookup(
|
function lookup(
|
||||||
@@ -187,6 +190,12 @@ function lookup(
|
|||||||
// 조건 값 = 1행 · 블록 오른쪽 한 칸 띄고
|
// 조건 값 = 1행 · 블록 오른쪽 한 칸 띄고
|
||||||
const tests: ((r: number) => string)[] = out ? [(r) => `NOT(ISBLANK(${out}${r}))`] : [];
|
const tests: ((r: number) => string)[] = out ? [(r) => `NOT(ISBLANK(${out}${r}))`] : [];
|
||||||
const ups: string[] = [];
|
const ups: string[] = [];
|
||||||
|
// 범위 경계 — 줄마다 「·범위규칙」 칸(이상 … = 아래 끝 포함 · … 미만 = 위 끝 뺌) · 없으면 초과 · 이하
|
||||||
|
const rc = at(RANGE_RULE);
|
||||||
|
const lowCmp = (c: string, r: number, x: string): string =>
|
||||||
|
rc ? `IF(LEFT(${rc}${r},2)="이상",${c}${r}<=${x},${c}${r}<${x})` : `${c}${r}<${x}`;
|
||||||
|
const highCmp = (c: string, r: number, x: string): string =>
|
||||||
|
rc ? `IF(RIGHT(${rc}${r},2)="미만",${x}<${c}${r},${x}<=${c}${r})` : `${x}<=${c}${r}`;
|
||||||
let bad = !out || rows < 1;
|
let bad = !out || rows < 1;
|
||||||
rule.conds.forEach((c, i) => {
|
rule.conds.forEach((c, i) => {
|
||||||
const ref = `$${colName(W + 1 + i)}$1`;
|
const ref = `$${colName(W + 1 + i)}$1`;
|
||||||
@@ -201,7 +210,7 @@ function lookup(
|
|||||||
tests.push(
|
tests.push(
|
||||||
(r) =>
|
(r) =>
|
||||||
`IF(AND(NOT(ISBLANK(${low}${r})),${low}${r}=${high}${r}),${ref}=${low}${r},` +
|
`IF(AND(NOT(ISBLANK(${low}${r})),${low}${r}=${high}${r}),${ref}=${low}${r},` +
|
||||||
`AND(OR(ISBLANK(${low}${r}),${low}${r}<${ref}),OR(ISBLANK(${high}${r}),${ref}<=${high}${r})))`,
|
`AND(OR(ISBLANK(${low}${r}),${lowCmp(low, r, ref)}),OR(ISBLANK(${high}${r}),${highCmp(high, r, ref)})))`,
|
||||||
);
|
);
|
||||||
} else bad = true;
|
} else bad = true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -216,9 +216,17 @@ def test_꼴_상자_선_규칙_배치() -> None:
|
|||||||
assert keyed and all(e["rule"]["field"] == "키" and "{" in e["ref"] for e in keyed)
|
assert keyed and all(e["rule"]["field"] == "키" and "{" in e["ref"] for e in keyed)
|
||||||
machine = [e for e in keyed if e["ref"].startswith("EQ:{기계}#")]
|
machine = [e for e in keyed if e["ref"].startswith("EQ:{기계}#")]
|
||||||
assert machine and {e["rule"]["from"]["name"] for e in machine} == {"기계"}
|
assert machine and {e["rule"]["from"]["name"] for e in machine} == {"기계"}
|
||||||
# 레미콘 {a}-{b}-{c} = 글 잇는 함수(&) → 재료 조건 찾기
|
# 「MO:경유:{지역}」 = 원문번호 글을 잇는 함수(&) → 유가 조건 찾기
|
||||||
|
assert any("&" in str(b.get("func")) for b in got["GC000267"]["flow"]["boxes"])
|
||||||
|
# 레미콘 {a}-{b}-{c} = 재료 조건 찾기 field 검색어 · 틀 · 자리 셋 · 구분 범위(정함 · 선 없음)
|
||||||
remi = got["GC000264"]["flow"]
|
remi = got["GC000264"]["flow"]
|
||||||
assert any("&" in str(b.get("func")) for b in remi["boxes"])
|
found = [e for b in remi["boxes"] for e in (b.get("input") or {}).get("elements", [])]
|
||||||
|
word = [e for e in found if (e.get("rule") or {}).get("field") == "검색어"]
|
||||||
|
assert word and word[0]["rule"]["template"] == "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}"
|
||||||
|
assert [x["name"] for x in word[0]["rule"]["slots"]] == ["굵은골재최대치수", "강도", "슬럼프"]
|
||||||
|
assert word[0]["scope"]["구분"] == "레미콘"
|
||||||
|
groups = {b["id"] for b in remi["boxes"] if (b.get("input") or {}).get("mode") == "group"}
|
||||||
|
assert not [ln for ln in remi["links"] if ln["to"]["box"] in groups]
|
||||||
# 덧줄 줄.'이름' · 비목 합
|
# 덧줄 줄.'이름' · 비목 합
|
||||||
assert got["GF000185"]["notes"] == [] or all(
|
assert got["GF000185"]["notes"] == [] or all(
|
||||||
not n.startswith("못 옮김") for n in got["GF000185"]["notes"]
|
not n.startswith("못 옮김") for n in got["GF000185"]["notes"]
|
||||||
|
|||||||
@@ -895,3 +895,23 @@ def test_표_찾기_여럿이면_위_끝_가장_작은_줄() -> None:
|
|||||||
)
|
)
|
||||||
assert got == [[11, None], [10, None], [9, None], [None, "표에 없는 조건"]]
|
assert got == [[11, None], [10, None], [9, None], [None, "표에 없는 조건"]]
|
||||||
assert [list(cp._lookup(rule, block, {"x": x})) for x in (1, 30, 60, 70)] == got
|
assert [list(cp._lookup(rule, block, {"x": x})) for x in (1, 30, 60, 70)] == got
|
||||||
|
|
||||||
|
|
||||||
|
def test_표_찾기_범위_경계_규칙_칸() -> None:
|
||||||
|
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp
|
||||||
|
|
||||||
|
head = ["h·아래", "h·위", "Q", "·범위규칙"]
|
||||||
|
block = [head, [None, 10, 1, "이상·미만"], [10, 20, 2, "이상·미만"], [20, 30, 3, "이상·이하"]]
|
||||||
|
rule = {
|
||||||
|
"kind": "lookup",
|
||||||
|
"conds": [{"col": "h", "from": {"box": "v", "name": "x"}}],
|
||||||
|
"col": "Q",
|
||||||
|
}
|
||||||
|
xs = [5, 10, 20, 30, 31]
|
||||||
|
got = _run(
|
||||||
|
RULE
|
||||||
|
+ f"const block = {json.dumps(block, ensure_ascii=False)};\nconst rule = {json.dumps(rule, ensure_ascii=False)};\n"
|
||||||
|
f"out({json.dumps(xs)}.map(x => {{ const r = R.applyRule(rule, block, {{ x }}); return [r.value, r.why]; }}));"
|
||||||
|
)
|
||||||
|
assert got == [[1, None], [2, None], [3, None], [3, None], [None, "표에 없는 조건"]]
|
||||||
|
assert [list(cp._lookup(rule, block, {"x": x})) for x in xs] == got
|
||||||
|
|||||||
Reference in New Issue
Block a user