Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
210 lines
7.3 KiB
Python
210 lines
7.3 KiB
Python
"""단가산출 흐름도 「식 보기」(`M02_DF_FormulaView.ts`) · 엑셀 「조건 만들기」 글 쪽(`spreadsheet_condition_text.ts`).
|
||
|
||
화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 상대 경로 TS(엔진 포함)는 이어 트랜스파일 · 화면 부품(@ui 의 elements · locale · modal)은 빈 흉내 · 엔진이 쓰는 @ui/sheet 는 그대로.
|
||
견본 함수 상자는 이 시험 안에 둠.
|
||
"""
|
||
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
VIEW = ROOT / "M02_MasterTemplete" / "DataFlow" / "M02_DF_FormulaView.ts"
|
||
TEXT = ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_condition_text.ts"
|
||
|
||
RUNNER = r"""
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const [root, viewFile, textFile] = process.argv.slice(1);
|
||
const ts = require(path.join(root, "config", "node_modules", "typescript"));
|
||
const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? 0 : () => undefined) });
|
||
const cache = {};
|
||
function load(f) {
|
||
if (cache[f]) return cache[f].exports;
|
||
const js = ts.transpileModule(fs.readFileSync(f, "utf8"), {
|
||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||
}).outputText;
|
||
const box = (cache[f] = { exports: {} });
|
||
const req = (id) => {
|
||
if (id.startsWith(".") || id.startsWith("@ui/sheet/")) {
|
||
const p = id.startsWith("@ui/sheet/")
|
||
? path.join(root, "ui_template", "sheet", id.slice("@ui/sheet/".length))
|
||
: path.resolve(path.dirname(f), id);
|
||
return fs.existsSync(p + ".ts") ? load(p + ".ts") : stub;
|
||
}
|
||
return stub;
|
||
};
|
||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||
return box.exports;
|
||
}
|
||
const v = load(viewFile);
|
||
const t = load(textFile);
|
||
const jobs = JSON.parse(fs.readFileSync(0, "utf8"));
|
||
const out = jobs.map((job) => {
|
||
if (job.op === "lines") {
|
||
const values = v.cellValuesOf(job.box, job.arrived);
|
||
return { values, lines: v.formulaLines(job.box, values) };
|
||
}
|
||
if (job.op === "toFormula") return t.draftToFormula(job.draft);
|
||
if (job.op === "toDraft") return t.formulaToDraft(job.formula);
|
||
return null;
|
||
});
|
||
process.stdout.write(JSON.stringify(out));
|
||
"""
|
||
|
||
|
||
def run(jobs: list[dict]) -> list:
|
||
if shutil.which("node") is None:
|
||
pytest.skip("node 가 없음")
|
||
done = subprocess.run(
|
||
["node", "-e", RUNNER, str(ROOT), str(VIEW), str(TEXT)],
|
||
input=json.dumps(jobs, ensure_ascii=False),
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
check=True,
|
||
)
|
||
return json.loads(done.stdout)
|
||
|
||
|
||
def func_box(cells: dict, inputs: list, variables: list, out_cell: str, out_name: str) -> dict:
|
||
return {
|
||
"id": "f1",
|
||
"kind": "function",
|
||
"name": "석공 품",
|
||
"color": "",
|
||
"unit": "인",
|
||
"x": 0,
|
||
"y": 0,
|
||
"func": {
|
||
"sheet": {"id": "s1", "이름": "석공 품", "칸": cells},
|
||
"inputs": inputs,
|
||
"variables": variables,
|
||
"output": {"cell": out_cell, "name": out_name, "unit": "인"},
|
||
},
|
||
}
|
||
|
||
|
||
def var(name: str, cell: str, example) -> dict:
|
||
return {"name": name, "type": "number", "unit": "-", "example": example, "cell": cell}
|
||
|
||
|
||
# 석공량(받는 알약 B1) · 높이(변수 B2) → B3 = 석공량 × (1 + 높이 ÷ 100)
|
||
PLAIN = func_box(
|
||
{
|
||
"A1": {"값": "석공량"},
|
||
"B3": {"식": "B1*(1+B2/100)"},
|
||
},
|
||
[{"port": "석공량", "cell": "B1"}],
|
||
[var("높이", "B2", 4)],
|
||
"B3",
|
||
"석공 인",
|
||
)
|
||
|
||
|
||
def test_이름_식과_값_식():
|
||
(got,) = run([{"op": "lines", "box": PLAIN, "arrived": {"석공량": 0.15}}])
|
||
assert got["values"]["B3"] == pytest.approx(0.156)
|
||
(line,) = got["lines"]
|
||
assert line["name"] == "석공 인"
|
||
assert line["named"] == "석공량 × (1 + 높이 ÷ 100)"
|
||
assert line["valued"] == "0.15 × (1 + 4 ÷ 100) = 0.156"
|
||
assert "branches" not in line
|
||
|
||
|
||
def test_중간_칸이_먼저_결과_칸이_마지막():
|
||
box = func_box(
|
||
{"B3": {"식": "B1*B2"}, "B4": {"식": "B3+1"}},
|
||
[{"port": "석공량", "cell": "B1"}],
|
||
[var("높이", "B2", 4)],
|
||
"B4",
|
||
"합",
|
||
)
|
||
(got,) = run([{"op": "lines", "box": box, "arrived": {"석공량": 2}}])
|
||
assert [x["name"] for x in got["lines"]] == ["B3", "합"] # 연결 없는 칸은 주소 그대로
|
||
assert got["lines"][1]["named"] == "B3 + 1"
|
||
assert got["lines"][1]["valued"] == "8 + 1 = 9"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("ground", "now"),
|
||
[("토사", [True, False]), ("암", [False, True])],
|
||
)
|
||
def test_IF_줄과_지금_갈래(ground, now):
|
||
box = func_box(
|
||
{"B3": {"식": 'IF(B4="토사",B1*2,B1*3)'}},
|
||
[{"port": "석공량", "cell": "B1"}],
|
||
[{"name": "지반", "type": "choice", "unit": "-", "example": ground, "cell": "B4"}],
|
||
"B3",
|
||
"결과",
|
||
)
|
||
(got,) = run([{"op": "lines", "box": box, "arrived": {"석공량": 10}}])
|
||
(line,) = got["lines"]
|
||
assert [b["now"] for b in line["branches"]] == now
|
||
assert line["branches"][0]["when"] == '지반 = "토사"'
|
||
assert line["branches"][0]["then"] == "석공량 × 2"
|
||
assert line["branches"][1] == {"when": "아니면", "then": "석공량 × 3", "now": now[1]}
|
||
assert line["valued"].endswith("= 20" if ground == "토사" else "= 30")
|
||
|
||
|
||
def test_조건_만들기_식과_다시_줄():
|
||
draft = {
|
||
"branches": [
|
||
{
|
||
"left": "B1",
|
||
"op": "=",
|
||
"right": {"kind": "value", "value": "토사"},
|
||
"then": {"kind": "cell", "cell": "C1"},
|
||
},
|
||
{
|
||
"left": "B1",
|
||
"op": "≥",
|
||
"right": {"kind": "value", "value": 3},
|
||
"then": {"kind": "value", "value": 0.5},
|
||
},
|
||
],
|
||
"otherwise": {"kind": "value", "value": 0},
|
||
}
|
||
(formula,) = run([{"op": "toFormula", "draft": draft}])
|
||
assert formula == 'IF(B1="토사",C1,IF(B1>=3,0.5,0))'
|
||
(back,) = run([{"op": "toDraft", "formula": formula}])
|
||
assert back == draft
|
||
|
||
|
||
@pytest.mark.parametrize("formula", ["SUM(B1:B2)", "IF(B1+1>2,1,2)", "IF(B1>2,B2*2,0)"])
|
||
def test_못_바꾸는_식은_null(formula):
|
||
(back,) = run([{"op": "toDraft", "formula": formula}])
|
||
assert back is None
|
||
|
||
|
||
def test_받는_칸은_선_규칙을_지난_값으로_그림():
|
||
# 시트에 남은 옛 값(99)이 아니라 규칙을 지나 들어온 값(arrived 0.15)으로 값 식을 그림
|
||
box = func_box(
|
||
{"B1": {"값": 99}, "B3": {"식": "B1*(1+B2/100)"}},
|
||
[{"port": "석공량", "cell": "B1"}],
|
||
[var("높이", "B2", 4)],
|
||
"B3",
|
||
"석공 인",
|
||
)
|
||
(got,) = run([{"op": "lines", "box": box, "arrived": {"석공량": 0.15}}])
|
||
assert got["lines"][0]["valued"] == "0.15 × (1 + 4 ÷ 100) = 0.156"
|
||
(stale,) = run([{"op": "lines", "box": box, "arrived": {}}])
|
||
assert stale["lines"][0]["valued"].startswith("99 ×") # 들어온 값이 없을 때만 칸 값
|
||
|
||
|
||
def test_오류_글은_따옴표_없이_값_없음():
|
||
box = func_box(
|
||
{"B1": {"식": "NA()"}, "B3": {"식": "B1+1"}},
|
||
[],
|
||
[],
|
||
"B3",
|
||
"합",
|
||
)
|
||
(got,) = run([{"op": "lines", "box": box, "arrived": {}}])
|
||
valued = [x["valued"] for x in got["lines"]]
|
||
assert all('"#' not in v for v in valued)
|
||
assert valued[-1] == "값 없음 + 1 = 값 없음"
|