Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
2026-10-02 00:25:50 +09:00
6 changed files with 104 additions and 64 deletions
+13 -5
View File
@@ -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]
+15 -9
View File
@@ -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)
+1 -1
View File
@@ -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 }));
}
+21 -5
View File
@@ -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 };