Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -332,14 +332,16 @@ def _rounded(value: Any, r: dict) -> Any:
|
||||
|
||||
def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
"""표에서 찾기 — 같음 · 범위(아래 < 값 ≤ 위 · 빈 끝 열림 · 아래 = 위면 같음) · 맞은 줄 하나
|
||||
· 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌(옛 엔진 find 와 같음 · Rule lookup 과 같음)."""
|
||||
· 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌 · 여럿이면 범위 위 끝이 가장 작은 줄
|
||||
(옛 엔진 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")
|
||||
if out not in head:
|
||||
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 = []
|
||||
tests, ups = [], []
|
||||
for c in rule.get("conds") or []:
|
||||
col, want = str(c.get("col")), _cond_of(conds, c.get("from") or {})
|
||||
if col in head:
|
||||
@@ -351,6 +353,7 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
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])
|
||||
@@ -365,13 +368,18 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
|
||||
tests.append(fits)
|
||||
else:
|
||||
return None, "오류"
|
||||
at = head.index(out)
|
||||
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
|
||||
|
||||
|
||||
@@ -594,7 +602,7 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
|
||||
for f, book, calc in zip(funcs, books, calcs):
|
||||
sheet = book["시트"][0]
|
||||
for cell, v in (calc.get(sheet.get("id")) or {}).items():
|
||||
if "오류" in v:
|
||||
if "오류" in v and (sheet.get("칸") or {}).get(cell, {}).get("식") != "NA()":
|
||||
errors.append(f"「{f.get('name')}」 {cell} 칸 계산 오류 {v['오류']}")
|
||||
out = f["func"].get("output") or {}
|
||||
at = _at(out.get("cell") or "A1")[2]
|
||||
|
||||
@@ -139,14 +139,15 @@ class _Flow:
|
||||
if len(texts) > 1:
|
||||
raise Unsupported(f"자리 표시가 여러 칸에 낀 요소 「{ref}」")
|
||||
((at, text),) = texts.items()
|
||||
holes = list(dict.fromkeys(_HOLE.findall(text)))
|
||||
if len(holes) == 1:
|
||||
src, hole = self.hole(holes[0]), holes[0]
|
||||
else: # 「레미콘 {a}-{b}-{c}」 — 함수 상자가 글을 이음
|
||||
src = self.func(f"{label} 찾는 말", ("@text", text))
|
||||
hole = src["name"]
|
||||
text = "{" + hole + "}"
|
||||
tpl = text if at is None else json.dumps(plain(ref | {at: text}), ensure_ascii=False)
|
||||
# 찾는 값 = 원문번호(「EQ:{기계}」 → 기계 · 「MO:경유:{지역}」 → "경유:"&지역)
|
||||
want = text.partition(":")[2] if at is None and ":" in text else text
|
||||
if _HOLE.fullmatch(want):
|
||||
src = self.hole(want[1:-1])
|
||||
else:
|
||||
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 += f"#{field}" if field else ""
|
||||
rule = {"field": "키", "op": "같음", "from": src}
|
||||
return ("@port", self.element(group, tpl, unit, f"{label} 자리", rule))
|
||||
@@ -295,6 +296,9 @@ class _Flow:
|
||||
group = GROUP_OF.get(base[1][:2], "기계")
|
||||
node2 = self.priced(group, base[1], "", f"{base[1]} {col}", col)
|
||||
return self.excel(node2, take, scope)
|
||||
if base[0] == "logic" and col == "계": # 다른 로직 출력은 비목 셋 — 계 = 그 합
|
||||
bid = self.logic(base)
|
||||
return self.excel(("@sum", [{"box": bid, "name": k} for k in COSTS]), take, scope)
|
||||
if base[0] == "logic":
|
||||
return take({"box": self.logic(base), "name": col}, f"{base[1]} {col}")
|
||||
if base[0] == "name" and base[1] == "줄" and f"줄.{col}" in scope:
|
||||
@@ -356,7 +360,9 @@ def _body(f: _Flow, row: dict) -> None:
|
||||
lines.setdefault(f"줄.{name}", []).append(part)
|
||||
continue
|
||||
unit = str(item.get("단위") or "")
|
||||
price = f.priced(str(item.get("종류")), item["요소"], unit, f"{name} 단가")
|
||||
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))
|
||||
parts[item["비목"]].append(part)
|
||||
lines.setdefault(f"줄.{name}", []).append(part)
|
||||
|
||||
@@ -241,7 +241,7 @@ function runFunc(
|
||||
if (v.cell) put(sheet.칸, v.cell, v.name in given ? given[v.name] : v.example);
|
||||
const values = recalcWorkbook({ 종류: "통합문서", 판: 1, 열: "DF", 서식: [{}], 시트: [sheet] });
|
||||
for (const [cell, v] of Object.entries(values[sheet.id] ?? {}))
|
||||
if ("오류" in v) {
|
||||
if ("오류" in v && sheet.칸[cell]?.식 !== "NA()") {
|
||||
const err = v.오류 === "#CYCLE!" ? `${v.오류} ${tl("Cycle")}` : v.오류;
|
||||
errors.push(tl("CellError", { box: box.name, cell, err }));
|
||||
}
|
||||
|
||||
@@ -181,9 +181,12 @@ function lookup(
|
||||
const i = head.indexOf(name);
|
||||
return i < 0 ? null : colName(i);
|
||||
};
|
||||
const out = at(rule.col);
|
||||
// 값 칸이 범위로 갈림(「이름·아래」 「이름·위」) — 아래 = 위면 그 값
|
||||
const two = !at(rule.col) && !!at(`${rule.col}·아래`) && !!at(`${rule.col}·위`);
|
||||
const out = two ? at(`${rule.col}·아래`) : at(rule.col);
|
||||
// 조건 값 = 1행 · 블록 오른쪽 한 칸 띄고
|
||||
const tests: ((r: number) => string)[] = out ? [(r) => `NOT(ISBLANK(${out}${r}))`] : [];
|
||||
const ups: string[] = [];
|
||||
let bad = !out || rows < 1;
|
||||
rule.conds.forEach((c, i) => {
|
||||
const ref = `$${colName(W + 1 + i)}$1`;
|
||||
@@ -193,13 +196,14 @@ function lookup(
|
||||
const low = at(`${c.col}·아래`);
|
||||
const high = at(`${c.col}·위`);
|
||||
if (same) tests.push((r) => `OR(ISBLANK(${same}${r}),${same}${r}=${ref})`);
|
||||
else if (low && high)
|
||||
else if (low && high) {
|
||||
ups.push(high);
|
||||
tests.push(
|
||||
(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})))`,
|
||||
);
|
||||
else bad = true;
|
||||
} else bad = true;
|
||||
});
|
||||
const hc = colName(W + 1 + rule.conds.length);
|
||||
const last = rows + 1;
|
||||
@@ -207,13 +211,25 @@ function lookup(
|
||||
칸[`${hc}${r}`] = {
|
||||
식: tests.length ? `IF(AND(${tests.map((t) => t(r)).join(",")}),1,0)` : "1",
|
||||
};
|
||||
const range = `${hc}2:${hc}${Math.max(2, last)}`;
|
||||
// 여럿이면 범위 위 끝이 가장 작은 줄(옛 find · 「20 이하」 「40 이하」 …) — 둘째 맞음 칸
|
||||
const hc2 = colName(W + 3 + rule.conds.length);
|
||||
const span = (c: string): string => `${c}2:${c}${Math.max(2, last)}`;
|
||||
for (let r = 2; r <= last; r++) {
|
||||
const top = ups.map((h) => `OR(ISBLANK(${h}${r}),${h}${r}=MINIFS(${span(h)},${span(hc)},1))`);
|
||||
칸[`${hc2}${r}`] = {
|
||||
식: top.length ? `IF(AND(${hc}${r}=1,${top.join(",")}),1,0)` : `${hc}${r}`,
|
||||
};
|
||||
}
|
||||
const range = span(hc2);
|
||||
const count = `${colName(W + 2 + rule.conds.length)}1`;
|
||||
const formula = out ? `INDEX(${out}2:${out}${Math.max(2, last)},MATCH(1,${range},0))` : "NA()";
|
||||
칸[count] = { 식: `SUM(${range})` };
|
||||
칸.ZZ1 = { 식: formula };
|
||||
const up = at(`${rule.col}·위`);
|
||||
if (two && up) 칸.ZZ2 = { 식: formula.replace(`INDEX(${out}2:${out}`, `INDEX(${up}2:${up}`) };
|
||||
if (bad) return { value: null, why: "오류", formula };
|
||||
const [n, got] = run(칸, [count, "ZZ1"]);
|
||||
const [n, got, top] = run(칸, [count, "ZZ1", "ZZ2"]);
|
||||
if (two && n === 1 && got !== top) return { value: null, why: "오류", formula };
|
||||
if (n === 0) return { value: null, why: "표에 없는 조건", formula };
|
||||
if (typeof n === "number" && n > 1) return { value: null, why: "표에 여럿 맞음", formula };
|
||||
if (isError(got)) return { value: null, why: "오류", formula };
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
· 전수 대조 — 사본 폴더(tmp/master_copy_36 · AISLO_MASTER_DIR)의 로직 1,472 마다
|
||||
옛 셈(master_formula.run) = 변환 흐름도 셈(Compute.run) · 입력 = check_master.시험입력(정함 ①)
|
||||
· 기본 입력(줄의 시험입력 뺌)도 같이 · 맞는 비율 · 안 맞는 까닭 → tmp/convert_report_36.json
|
||||
· 조건 찾기 field 「키」(입력값으로 요소 고르기 · 정함 ②) · 표 값 식 글을 수로(정함 ③)는 sub4 몫
|
||||
— 대조는 같은 뜻 대역(ref 의 {…} 에 알약 값을 끼워 dv.one_value · 식 글은 옛 파서로 셈)
|
||||
· 같은 물음은 한 번만 셈
|
||||
· 조건 찾기 field 「키」(입력값으로 요소 고르기 · 정함 ②) · 표 값 식 글 → 수(정함 ③)는 서버 길로
|
||||
(Compute · Values · sub4) · 값 찾기(dv.one_value)는 같은 물음이면 한 번만 셈
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -26,8 +25,7 @@ WORKERS = 6
|
||||
|
||||
# 일꾼(따로 뜬 파이썬 · 사본 폴더) — 표준 입력 = 키 목록 JSON · 표준 출력 = 줄마다 결과 JSON
|
||||
WORKER = r"""
|
||||
import json, os, re, sys
|
||||
from decimal import Decimal
|
||||
import json, os, sys
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp, M02_DF_Convert as cv
|
||||
@@ -40,41 +38,8 @@ def cached(item, local=None):
|
||||
k = json.dumps(item, ensure_ascii=False, sort_keys=True, default=str)
|
||||
if k not in seen:
|
||||
seen[k] = one_value(item, local)
|
||||
if seen[k].get("rows"): # 표 값 식 글 → 수(Values._table 대역)
|
||||
seen[k]["rows"] = [[arith(x) for x in r] for r in seen[k]["rows"]]
|
||||
return seen[k]
|
||||
def arith(x):
|
||||
if isinstance(x, str) and mf._ARITH.fullmatch(x):
|
||||
try:
|
||||
return mf.evaluate(mf.parse(x), {}, master)
|
||||
except (mf.FormulaError, ArithmeticError):
|
||||
return x
|
||||
return x
|
||||
dv.one_value = cached
|
||||
element_value = cp._element_value
|
||||
HOLE = re.compile(r"\{([^{}]+)\}")
|
||||
def stand_in(group, el, given, ports, errors, box, ctx):
|
||||
rule = el.get("rule") or {}
|
||||
if el.get("mode") != "rule" or rule.get("field") != "키":
|
||||
return element_value(group, el, given, ports, errors, box, ctx)
|
||||
src = rule.get("from") or {}
|
||||
v = ports.get(src.get("box"), {}).get(src.get("name"))
|
||||
if v is None:
|
||||
errors.append(f"「{el.get('name')}」 자리 값 없음")
|
||||
return None
|
||||
text = format(Decimal(str(v)).normalize(), "f") if isinstance(v, (int, float)) else str(v)
|
||||
ref, _, field = HOLE.sub(lambda m: text, el["ref"]).partition("#")
|
||||
try:
|
||||
ref = ref if ref.lstrip().startswith("{") else str(master.get(ref).get("키"))
|
||||
except mf.FormulaError as e:
|
||||
errors.append(f"「{el.get('name')}」 {e}")
|
||||
return None
|
||||
ref += f"#{field}" if field else ""
|
||||
got = cached({"group": group, "ref": ref, "unit": el.get("unit")})
|
||||
if not got["ok"]:
|
||||
errors.append(f"「{el.get('name')}」 {got.get('reason')}")
|
||||
return got.get("value") if got["ok"] else None
|
||||
cp._element_value = stand_in
|
||||
def load(key):
|
||||
if key not in flows:
|
||||
flows[key] = cv.convert(master.logic(key))["flow"]
|
||||
@@ -95,7 +60,8 @@ def check(row, given):
|
||||
got = cv.convert(row)
|
||||
flows[row["키"]] = got["flow"]
|
||||
try:
|
||||
design = {"in-design": {k: cv.plain(v) for k, v in given.items()}}
|
||||
env = mf._inputs(row, given) # 옛 엔진처럼 글로 적힌 수 → 수(화면은 수로 넘김)
|
||||
design = {"in-design": {k: cv.plain(v) for k, v in env.items()}}
|
||||
new = cp.run(got["flow"], design, load, [])
|
||||
except Exception as e:
|
||||
return {"state": "흐름 셈 멈춤", "why": repr(e)[:200], "notes": got["notes"]}
|
||||
@@ -156,17 +122,24 @@ def _keys() -> list[str]:
|
||||
return json.loads(_run(code))
|
||||
|
||||
|
||||
REASONS = [ # (알림에 든 말, 까닭) — 앞이 먼저
|
||||
("표에 여럿 맞음", "표 범위 규칙 다름 — 여럿 맞음(이상·이하 · 이상·미만 경계)"),
|
||||
("표에 없는 조건", "표 범위 규칙 다름 — 없는 조건(이상·이하 · 이상·미만 경계)"),
|
||||
("같음", "재료 검색어 자리 — 서버 조건 찾기(키 같음) ≠ 옛 검색어 찾기"),
|
||||
("맞는 재료 없음", "재료 빈 검색어 — 서버 재료 값 찾기 다름"),
|
||||
("비어 있음", "단가 빈 품목 — 서버 처리 다름"),
|
||||
("값 없음", "단가 빈 품목 — 서버 처리 다름"),
|
||||
]
|
||||
|
||||
|
||||
def _reason(r: dict) -> str:
|
||||
if r["state"] != "다름":
|
||||
return r["state"]
|
||||
moved = [n for n in r.get("notes") or [] if n.startswith("못 옮김")]
|
||||
if moved:
|
||||
return "못 옮김 — " + moved[0].split("—", 1)[1].strip().split("「")[0].strip()
|
||||
for e in r.get("errors") or []:
|
||||
tail = e.rsplit("」", 1)[-1].strip()
|
||||
if tail:
|
||||
return "흐름 셈 알림 — " + tail.split("—")[0].strip()[:40]
|
||||
return "값 다름(알림 없음)"
|
||||
text = " | ".join(r.get("errors") or [])
|
||||
return next((why for word, why in REASONS if word in text), "값 다름(알림 없음)")
|
||||
|
||||
|
||||
# ── 꼴 시험 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -858,3 +858,40 @@ def test_표_찾기_빈_조건칸_무엇이든_빈_값줄_건너뜀_실패는_
|
||||
want = None if isinstance(screen_out, str) and screen_out.startswith("#") else screen_out
|
||||
assert (None if out is None else float(out)) == (None if want is None else float(want))
|
||||
assert [b["kind"] for b in srv["breaks"]] == screen_breaks
|
||||
|
||||
|
||||
def test_표_찾기_값_칸이_범위로_갈림_아래_같으면_그_값() -> None:
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp
|
||||
|
||||
block = [["구분", "값·아래", "값·위"], ["가", 3, 3], ["나", 1, 2]]
|
||||
rule = {
|
||||
"kind": "lookup",
|
||||
"conds": [{"col": "구분", "from": {"box": "v", "name": "구분"}}],
|
||||
"col": "값",
|
||||
}
|
||||
got = _run(
|
||||
RULE
|
||||
+ f"const block = {json.dumps(block, ensure_ascii=False)};\n"
|
||||
+ f"const rule = {json.dumps(rule, ensure_ascii=False)};\n"
|
||||
"out(['가', '나'].map(g => { const r = R.applyRule(rule, block, { 구분: g }); return [r.value, r.why]; }));"
|
||||
)
|
||||
assert got == [[3, None], [None, "오류"]]
|
||||
assert [list(cp._lookup(rule, block, {"구분": g})) for g in ("가", "나")] == got
|
||||
|
||||
|
||||
def test_표_찾기_여럿이면_위_끝_가장_작은_줄() -> None:
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp
|
||||
|
||||
block = [["거리·아래", "거리·위", "Q"], [None, 20, 11], [None, 40, 10], [None, 60, 9]]
|
||||
rule = {
|
||||
"kind": "lookup",
|
||||
"conds": [{"col": "거리", "from": {"box": "v", "name": "x"}}],
|
||||
"col": "Q",
|
||||
}
|
||||
got = _run(
|
||||
RULE
|
||||
+ f"const block = {json.dumps(block)};\nconst rule = {json.dumps(rule, ensure_ascii=False)};\n"
|
||||
"out([1, 30, 60, 70].map(x => { const r = R.applyRule(rule, block, { x }); return [r.value, r.why]; }));"
|
||||
)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user