feat(M02-흐름도): 식 보기 FormulaView · 엑셀 조건 만들기 spreadsheet_condition(우클릭 · conditions 줄 때만) + 시험
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""단가산출 흐름도 「식 보기」(`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
|
||||
Reference in New Issue
Block a user