"""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"))