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:
@@ -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"))
|
||||
Reference in New Issue
Block a user