feat(m02-df): 36-0b 선 조건 여러 줄(그리고 · 또는) · 값 목록
- CfRule gate conds[{from · op · value}] · join 그리고/또는 · 옛 한 줄 꼴(from · op · value)도 읽음 — 옛 칸은 첫 조건과 같게 둠(makeGate)
- gateConds · makeGate · valueChoices(변수 고르기 · 재료 규격 · 표 칸 값 · 수 · 함수 결과는 null)
- 규칙 → 받는 칸 엑셀 식 IF(AND/OR(...),A1,NA()) · 선 이름표 「지반 = 토사 그리고 높이 ≥ 3 일 때만」 · 겹침 · 안 맞음은 조건 알약 묶음끼리
- 조건 값 열쇠 「상자|알약」 먼저(이름 겹침 막음) · M02_DF_Compute.py gate 같은 뜻(서버 = 화면 시험)
- 시험 test_m02_df_model 30개
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
@@ -323,7 +323,7 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
return None, "오류"
|
||||
tests = []
|
||||
for c in rule.get("conds") or []:
|
||||
col, want = str(c.get("col")), conds.get((c.get("from") or {}).get("name"))
|
||||
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: _same(row[i], want))
|
||||
@@ -362,12 +362,13 @@ def apply_rule(rule: dict, value: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
if kind == "lookup":
|
||||
return _lookup(rule, value, conds)
|
||||
if kind == "gate":
|
||||
src = (rule.get("from") or {}).get("name")
|
||||
return (
|
||||
(value, None)
|
||||
if _compare(conds.get(src), rule.get("op"), rule.get("value"))
|
||||
else (None, "통과 안 됨")
|
||||
)
|
||||
rows, join = _gate_conds(rule)
|
||||
hits = [
|
||||
_compare(_cond_of(conds, c.get("from") or {}), c.get("op"), c.get("value"))
|
||||
for c in rows
|
||||
]
|
||||
ok = any(hits) if join == "또는" else all(hits)
|
||||
return (value, None) if ok else (None, "통과 안 됨")
|
||||
n = _num_of(value)
|
||||
if n is None:
|
||||
return (None, None) if value is None else (None, "오류")
|
||||
@@ -387,7 +388,23 @@ def _sources(rule: dict | None) -> list[dict]:
|
||||
return []
|
||||
if rule.get("kind") == "lookup":
|
||||
return [c.get("from") or {} for c in rule.get("conds") or []]
|
||||
return [rule.get("from") or {}] if rule.get("kind") == "gate" else []
|
||||
if rule.get("kind") == "gate":
|
||||
return [c.get("from") or {} for c in _gate_conds(rule)[0]]
|
||||
return []
|
||||
|
||||
|
||||
def _gate_conds(rule: dict) -> tuple[list[dict], str]:
|
||||
"""조건 통과 줄 · 잇는 말 — conds · join · 옛 한 줄 꼴은 줄 하나(Model gateConds)."""
|
||||
rows = rule.get("conds") or [
|
||||
{"from": rule.get("from") or {}, "op": rule.get("op"), "value": rule.get("value")}
|
||||
]
|
||||
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` 와 같은 차례) ─────────────────────────────
|
||||
@@ -462,9 +479,11 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
|
||||
return v
|
||||
if v is None and rule.get("kind") in ("scale", "round"):
|
||||
return None
|
||||
conds = {
|
||||
p.get("name"): ports.get(p.get("box"), {}).get(p.get("name")) for p in _sources(rule)
|
||||
}
|
||||
conds = {}
|
||||
for p in _sources(rule):
|
||||
got_v = ports.get(p.get("box"), {}).get(p.get("name"))
|
||||
conds[p.get("name")] = got_v
|
||||
conds[f"{p.get('box')}|{p.get('name')}"] = got_v
|
||||
got, why = apply_rule(rule, v, conds)
|
||||
if why:
|
||||
blocked.add(ln["to"].get("box"))
|
||||
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
depends,
|
||||
designInputs,
|
||||
elementPort,
|
||||
gateConds,
|
||||
portsIn,
|
||||
portsOut,
|
||||
ruleSources,
|
||||
type CfBox,
|
||||
type CfGateRule,
|
||||
type CfLink,
|
||||
type CfLogic,
|
||||
} from "./M02_DF_Model";
|
||||
@@ -266,7 +268,10 @@ export function evalLogic(
|
||||
if (!l.rule) return v;
|
||||
if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null;
|
||||
const conds = Object.fromEntries(
|
||||
ruleSources(l.rule).map((p) => [p.name, valueOf(p.box, p.name)]),
|
||||
ruleSources(l.rule).flatMap((p) => [
|
||||
[p.name, valueOf(p.box, p.name)],
|
||||
[`${p.box}|${p.name}`, valueOf(p.box, p.name)],
|
||||
]),
|
||||
);
|
||||
const res = applyRule(l.rule, v, conds);
|
||||
if (l.rule.kind === "gate") passed.set(l.id, res.why === null);
|
||||
@@ -411,21 +416,26 @@ function gates(
|
||||
const groups = new Map<string, CfLink[]>();
|
||||
for (const l of lg.links)
|
||||
if (l.rule?.kind === "gate") {
|
||||
const k = `${l.rule.from.box}|${l.rule.from.name}|${l.from.box}|${l.from.name}`;
|
||||
// 같은 알약에서 나가 같은 조건 알약들로 가르는 선끼리 — 여러 조건이면 조건 알약 묶음이 같을 때
|
||||
const srcs = ruleSources(l.rule)
|
||||
.map((p) => `${p.box}|${p.name}`)
|
||||
.sort()
|
||||
.join("+");
|
||||
const k = `${srcs}|${l.from.box}|${l.from.name}`;
|
||||
groups.set(k, [...(groups.get(k) ?? []), l]);
|
||||
}
|
||||
for (const list of groups.values()) {
|
||||
if (list.length < 2) continue;
|
||||
const n = list.filter((l) => passed.get(l.id)).length;
|
||||
if (n === 1) continue;
|
||||
const g = list[0].rule as Extract<CfLink["rule"], { kind: "gate" }>;
|
||||
const fill = { box: name(g.from.box), pill: g.from.name };
|
||||
const g = gateConds(list[0].rule as CfGateRule).conds[0].from;
|
||||
const fill = { box: name(g.box), pill: g.name };
|
||||
const text = tl(n > 1 ? "Overlap" : "NoneMatch", fill);
|
||||
found.push({
|
||||
kind: n > 1 ? "조건 겹침" : "조건 안 맞음",
|
||||
level: "문제",
|
||||
box: g.from.box,
|
||||
port: g.from.name,
|
||||
box: g.box,
|
||||
port: g.name,
|
||||
text,
|
||||
});
|
||||
errors.push(text);
|
||||
|
||||
@@ -126,9 +126,27 @@ export type CfGateOp = "=" | "≠" | "≥" | "≤" | ">" | "<";
|
||||
/** 선 규칙(선 하나에 하나 · 없으면 그대로) — 받는 칸의 엑셀 수식으로 바뀌어 엔진 한 벌로 셈
|
||||
* lookup = 보낸 표에서 조건 칸마다 맞출 알약 · 꺼낼 칸(범위 칸은 「이름·아래」 「이름·위」)
|
||||
* gate = 알약 값이 조건을 통과할 때만 보냄 · scale = × ÷ +% -% · round = 끝수 */
|
||||
/** 조건 통과의 조건 한 줄 */
|
||||
export interface CfGateCond {
|
||||
from: CfPort;
|
||||
op: CfGateOp;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
/** 조건 통과 — conds 여러 줄을 join(그리고 · 또는)으로 · 옛 한 줄 꼴(from · op · value)도 읽음
|
||||
* from · op · value 는 첫 조건과 같게 둠(옛 화면 코드가 그대로 돌게 · makeGate 가 맞춰 줌) — conds 가 있으면 conds 가 이김 */
|
||||
export type CfGateRule = {
|
||||
kind: "gate";
|
||||
conds?: CfGateCond[];
|
||||
join?: "그리고" | "또는";
|
||||
from: CfPort;
|
||||
op: CfGateOp;
|
||||
value: number | string;
|
||||
};
|
||||
|
||||
export type CfRule =
|
||||
| { kind: "lookup"; conds: { col: string; from: CfPort }[]; col: string }
|
||||
| { kind: "gate"; from: CfPort; op: CfGateOp; value: number | string }
|
||||
| CfGateRule
|
||||
| { kind: "scale"; op: "×" | "÷" | "+%" | "-%"; value: number }
|
||||
| ({ kind: "round" } & CfRound);
|
||||
|
||||
@@ -357,10 +375,65 @@ export function portDir(box: CfBox, name: string): "in" | "out" {
|
||||
export function ruleSources(rule: CfRule | undefined): CfPort[] {
|
||||
if (!rule) return [];
|
||||
if (rule.kind === "lookup") return rule.conds.map((c) => c.from);
|
||||
if (rule.kind === "gate") return [rule.from];
|
||||
if (rule.kind === "gate") return gateConds(rule).conds.map((c) => c.from);
|
||||
return [];
|
||||
}
|
||||
|
||||
/** 조건 통과의 조건 줄 · 잇는 말 — 옛 한 줄 꼴은 조건 하나 */
|
||||
export function gateConds(rule: CfGateRule): { conds: CfGateCond[]; join: "그리고" | "또는" } {
|
||||
const conds = rule.conds?.length
|
||||
? rule.conds
|
||||
: [{ from: rule.from, op: rule.op, value: rule.value }];
|
||||
return { conds, join: rule.join ?? "그리고" };
|
||||
}
|
||||
|
||||
/** 조건 통과 만들기 — 옛 칸(from · op · value)은 첫 조건과 같게 */
|
||||
export function makeGate(conds: CfGateCond[], join: "그리고" | "또는" = "그리고"): CfGateRule {
|
||||
const first = conds[0] ?? { from: { box: "", name: "" }, op: "=" as const, value: "" };
|
||||
return {
|
||||
kind: "gate",
|
||||
conds: clone(conds),
|
||||
join,
|
||||
from: { ...first.from },
|
||||
op: first.op,
|
||||
value: first.value,
|
||||
};
|
||||
}
|
||||
|
||||
/** 알약이 가질 수 있는 값 목록 — 조건 값 고르기 · null = 숫자 칸(범위 검사)
|
||||
* 입력 변수 고르기 = choices · 재료 알약 = 그 요소들의 규격 · 표 알약 = opts.col 칸 값들(opts.values 의 표 덩어리)
|
||||
* · 함수 결과 · 다른 로직 출력 · 수 변수 = null */
|
||||
export function valueChoices(
|
||||
lg: CfLogic,
|
||||
port: CfPort,
|
||||
opts: { col?: string; values?: Record<string, Record<string, unknown>> } = {},
|
||||
): (string | number)[] | null {
|
||||
const box = boxOf(lg, port.box);
|
||||
const inp = box?.input;
|
||||
if (!box || box.kind !== "input" || !inp || inp.mode === "logic") return null;
|
||||
const v = inp.variables.find((x) => x.name === port.name);
|
||||
if (v) return v.type === "choice" && v.choices?.length ? [...v.choices] : null;
|
||||
if (port.name === TABLE_PORT && inp.table) {
|
||||
const block = opts.values?.[box.id]?.[TABLE_PORT];
|
||||
if (!opts.col || !Array.isArray(block) || !Array.isArray(block[0])) return null;
|
||||
const head = (block[0] as unknown[]).map(String);
|
||||
const at = head.indexOf(opts.col);
|
||||
if (at < 0) return null;
|
||||
const seen: (string | number)[] = [];
|
||||
for (const row of block.slice(1) as unknown[][]) {
|
||||
const x = row[at];
|
||||
if ((typeof x === "number" || typeof x === "string") && x !== "" && !seen.includes(x))
|
||||
seen.push(x);
|
||||
}
|
||||
return seen.length ? seen : null;
|
||||
}
|
||||
const specs = inp.elements
|
||||
.filter((e) => elementPort(e) === port.name && e.spec.trim())
|
||||
.map((e) => e.spec.trim());
|
||||
const uniq = [...new Set(specs)];
|
||||
return uniq.length ? uniq : null;
|
||||
}
|
||||
|
||||
/** 상자 사이 기댐 — 선 + 선 규칙의 조건 알약 + 재료 조건 찾기의 알약(→ 그 상자) */
|
||||
export function depends(lg: CfLogic): { from: string; to: string }[] {
|
||||
const out: { from: string; to: string }[] = [];
|
||||
|
||||
@@ -12,7 +12,7 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { colName } from "../../A00_Common/spreadsheet/spreadsheet_address";
|
||||
import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc";
|
||||
import type { CalcValue, Cell } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { CfGateOp, CfRound, CfRule } from "./M02_DF_Model";
|
||||
import { gateConds, type CfGateOp, type CfRound, type CfRule } from "./M02_DF_Model";
|
||||
|
||||
type Scalar = number | string | boolean | null;
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface CfRuleResult {
|
||||
const TEXT = {
|
||||
Lookup: ["표에서 찾기", "Look up in table"],
|
||||
Gate: ["일 때만", "only when"],
|
||||
And: ["그리고", "and"],
|
||||
Or: ["또는", "or"],
|
||||
Places: ["소수 {n}자리", "{n} decimals"],
|
||||
Round: ["반올림", "round"],
|
||||
Up: ["올림", "round up"],
|
||||
@@ -57,8 +59,11 @@ export function ruleLabel(rule: CfRule): string {
|
||||
const conds = rule.conds.map((c) => `${c.col} = ${c.from.name}`).join(" · ");
|
||||
return `${tl("Lookup")}${conds ? ` (${conds})` : ""} → ${rule.col}`;
|
||||
}
|
||||
case "gate":
|
||||
return `${rule.from.name} ${rule.op} ${rule.value} ${tl("Gate")}`;
|
||||
case "gate": {
|
||||
const g = gateConds(rule);
|
||||
const join = g.join === "또는" ? tl("Or") : tl("And");
|
||||
return `${g.conds.map((c) => `${c.from.name} ${c.op} ${c.value}`).join(` ${join} `)} ${tl("Gate")}`;
|
||||
}
|
||||
case "scale":
|
||||
return rule.op === "+%" || rule.op === "-%"
|
||||
? `${rule.op[0]}${rule.value}%`
|
||||
@@ -109,6 +114,10 @@ const lit = (v: number | string): string =>
|
||||
|
||||
/* ── 규칙 ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** 조건 알약 값 — 열쇠 「상자|알약」 이 먼저 · 없으면 알약 이름 */
|
||||
const condOf = (conds: Record<string, unknown>, p: { box: string; name: string }): unknown =>
|
||||
`${p.box}|${p.name}` in conds ? conds[`${p.box}|${p.name}`] : conds[p.name];
|
||||
|
||||
/** 규칙 적용 — value = 보낸 알약 값(표면 머리 줄 포함 칸 덩어리) · conds = 조건 알약 이름 → 값(lookup · gate) */
|
||||
export function applyRule(
|
||||
rule: CfRule,
|
||||
@@ -121,9 +130,17 @@ export function applyRule(
|
||||
if (a1) 칸.A1 = a1;
|
||||
let formula: string;
|
||||
if (rule.kind === "gate") {
|
||||
const c1 = cellOf(conds[rule.from.name]);
|
||||
if (c1) 칸.C1 = c1;
|
||||
formula = `IF(C1${OP[rule.op]}${lit(rule.value)},A1,NA())`;
|
||||
// 조건 값 = C1 · D1 · E1 … · 여러 줄은 AND / OR
|
||||
const g = gateConds(rule);
|
||||
const tests = g.conds.map((c, i) => {
|
||||
const at = `${colName(2 + i)}1`;
|
||||
const cv = cellOf(condOf(conds, c.from));
|
||||
if (cv) 칸[at] = cv;
|
||||
return `${at}${OP[c.op]}${lit(c.value)}`;
|
||||
});
|
||||
const test =
|
||||
tests.length > 1 ? `${g.join === "또는" ? "OR" : "AND"}(${tests.join(",")})` : tests[0];
|
||||
formula = `IF(${test},A1,NA())`;
|
||||
} else if (rule.kind === "scale") {
|
||||
const v = rule.value;
|
||||
formula =
|
||||
@@ -169,7 +186,7 @@ function lookup(
|
||||
let bad = !out || rows < 1;
|
||||
rule.conds.forEach((c, i) => {
|
||||
const ref = `$${colName(W + 1 + i)}$1`;
|
||||
const cv = cellOf(conds[c.from.name]);
|
||||
const cv = cellOf(condOf(conds, c.from));
|
||||
if (cv) 칸[`${colName(W + 1 + i)}1`] = cv;
|
||||
const same = at(c.col);
|
||||
const low = at(`${c.col}·아래`);
|
||||
|
||||
@@ -680,3 +680,107 @@ def test_만나는_상자는_살아_있는_선이_있으면_셈_서버와_같음
|
||||
k: float(v) for k, v in screen_out.items()
|
||||
}
|
||||
assert [[b["kind"], b["level"], b["link"]] for b in srv["breaks"]] == screen_breaks
|
||||
|
||||
|
||||
# ── 36-0b 선 조건 여러 줄(그리고 · 또는) · 값 목록 ─────────────────────────────
|
||||
def test_조건_여러_줄_그리고_또는_글_식_서버같음() -> None:
|
||||
lg = _ground()
|
||||
two = {
|
||||
"kind": "gate",
|
||||
"conds": [
|
||||
{"from": {"box": "in-ground", "name": "지반"}, "op": "=", "value": "토사"},
|
||||
{"from": {"box": "in-ground", "name": "굴착량"}, "op": "≥", "value": 50},
|
||||
],
|
||||
"join": "그리고",
|
||||
"from": {"box": "in-ground", "name": "지반"},
|
||||
"op": "=",
|
||||
"value": "토사",
|
||||
}
|
||||
either = {**two, "join": "또는"}
|
||||
cases = [
|
||||
(two, {"in-ground": {"지반": "토사", "굴착량": 100}}),
|
||||
(two, {"in-ground": {"지반": "토사", "굴착량": 10}}),
|
||||
(either, {"in-ground": {"지반": "암", "굴착량": 100}}),
|
||||
(either, {"in-ground": {"지반": "암", "굴착량": 10}}),
|
||||
]
|
||||
got = _run(
|
||||
RULE
|
||||
+ f"const base = {json.dumps(lg, ensure_ascii=False)};\n"
|
||||
+ f"const cases = {json.dumps(cases, ensure_ascii=False)};\n"
|
||||
"out(cases.map(([rule, vals]) => {\n"
|
||||
" const lg = { ...base, links: base.links.map(l => l.id === 'l1' ? { ...l, rule } : l) };\n"
|
||||
" const ev = E.evalLogic(lg, vals);\n"
|
||||
" return [R.ruleLabel(rule), R.applyRule(rule, 1, { 지반: vals['in-ground'].지반, 굴착량: vals['in-ground'].굴착량 }).formula,"
|
||||
" ev.ports['fn-exc'], ev.breaks.filter(b => b.link === 'l1').map(b => b.kind)];\n"
|
||||
"}));"
|
||||
)
|
||||
labels = [g[0] for g in got]
|
||||
assert labels[0] == "지반 = 토사 그리고 굴착량 ≥ 50 일 때만"
|
||||
assert labels[2] == "지반 = 토사 또는 굴착량 ≥ 50 일 때만"
|
||||
assert got[0][1] == 'IF(AND(C1="토사",D1>=50),A1,NA())'
|
||||
assert got[2][1] == 'IF(OR(C1="토사",D1>=50),A1,NA())'
|
||||
passed = [g[3] == [] for g in got]
|
||||
assert passed == [True, False, True, False]
|
||||
# 서버(Compute) = 화면(Eval)
|
||||
for (rule, values), (_, _, ports, breaks) in zip(cases, got):
|
||||
srv_lg = {
|
||||
**lg,
|
||||
"links": [{**ln, "rule": rule} if ln["id"] == "l1" else ln for ln in lg["links"]],
|
||||
}
|
||||
srv = _server(srv_lg, values)
|
||||
exc = srv["ports"].get("fn-exc") or {}
|
||||
assert {k: (None if v is None else float(v)) for k, v in exc.items()} == {
|
||||
k: (None if v is None else float(v)) for k, v in ports.items()
|
||||
}
|
||||
assert [b["kind"] for b in srv["breaks"] if b.get("link") == "l1"] == breaks
|
||||
|
||||
|
||||
def test_옛_한_줄_꼴과_makeGate() -> None:
|
||||
got = _run(
|
||||
RULE
|
||||
+ "const old = { kind: 'gate', from: { box: 'b1', name: '지반' }, op: '=', value: '토사' };\n"
|
||||
"const made = M.makeGate([{ from: { box: 'b1', name: '지반' }, op: '≠', value: '암' }, { from: { box: 'b1', name: '높이' }, op: '<', value: 3 }], '또는');\n"
|
||||
"out([M.gateConds(old), R.ruleLabel(old), made.from, made.op, made.value, M.ruleSources(made).map(p => p.name),"
|
||||
" R.applyRule(made, 7, { 지반: '암', 높이: 2 }).value, R.applyRule(made, 7, { 지반: '암', 높이: 5 }).why]);"
|
||||
)
|
||||
conds, label, first_from, first_op, first_value, sources, ok, blocked = got
|
||||
assert conds == {
|
||||
"conds": [{"from": {"box": "b1", "name": "지반"}, "op": "=", "value": "토사"}],
|
||||
"join": "그리고",
|
||||
}
|
||||
assert label == "지반 = 토사 일 때만"
|
||||
assert (first_from, first_op, first_value) == ({"box": "b1", "name": "지반"}, "≠", "암")
|
||||
assert sources == ["지반", "높이"]
|
||||
assert (ok, blocked) == (7, "통과 안 됨")
|
||||
|
||||
|
||||
def test_값_목록() -> None:
|
||||
got = _run(
|
||||
PIPE
|
||||
+ "lg = M.updateBox(lg, dia.id, { input: { ...lg.boxes[0].input, variables: [...lg.boxes[0].input.variables,"
|
||||
" { name: '지반', type: 'choice', unit: '-', choices: ['토사', '암'], example: '토사' }] } });\n"
|
||||
"lg = M.updateBox(lg, mat.id, { input: { ...lg.boxes[1].input, elements: lg.boxes[1].input.elements.map((e, i) => i === 1 ? { ...e, spec: '12mm' } : e) } });\n"
|
||||
"r = add(lg, 'input'); lg = r.lg; const tb = r.box;\n"
|
||||
"lg = M.updateBox(lg, tb.id, { input: { mode: 'group', group: '소요량', elements: [], variables: [], table: { key: 'QF1' } } });\n"
|
||||
"const vals = { [tb.id]: { 표: [['돌', '석공'], ['깬돌', 0.2], ['견치돌', 0.3], ['깬돌', 0.25]] } };\n"
|
||||
"const v = (box, name, opts) => M.valueChoices(lg, { box, name }, opts);\n"
|
||||
"out([v(dia.id, '지반'), v(dia.id, '관경'), v(mat.id, '거푸집 자리'), v(mat.id, '흄관 단가'),"
|
||||
" v(tb.id, '표', { col: '돌', values: vals }), v(tb.id, '표', { col: '없음', values: vals }), v(tb.id, '표'),"
|
||||
" v(f.id, '재료값'), v('없는 상자', '지반')]);"
|
||||
)
|
||||
assert got == [["토사", "암"], None, ["12mm"], None, ["깬돌", "견치돌"], None, None, None, None]
|
||||
|
||||
|
||||
def test_조건_여러_줄_겹침_안맞음() -> None:
|
||||
got = _run(
|
||||
SPLIT
|
||||
+ "const both = (op1, v1) => M.makeGate([{ from: { box: v.id, name: '지반' }, op: op1, value: v1 }, { from: { box: v.id, name: '높이' }, op: '≥', value: 3 }]);\n"
|
||||
"let g = M.setRule(lg, lg.links[0].id, both('=', '토사')).lg;\n"
|
||||
"g = M.setRule(g, lg.links[1].id, both('≠', '암')).lg;\n"
|
||||
"const over = E.breaks(g, V('토사')).filter(b => b.level === '문제').map(b => b.kind);\n"
|
||||
"const none = E.breaks(g, V('암', 2)).filter(b => b.level === '문제').map(b => b.kind);\n"
|
||||
"out([over, none]);"
|
||||
)
|
||||
over, none = got
|
||||
assert "조건 겹침" in over
|
||||
assert "조건 안 맞음" in none
|
||||
|
||||
Reference in New Issue
Block a user