- 잠금 카드 수량 식 속 표 찾기를 엔진 문법대로 읽음(따옴표 · 괄호 · 겹친 찾기) · 못 읽는 꼴은 한 줄 안내 - 표를 그대로(머리 · 줄) · 지금 시험값으로 걸린 줄 노랑(서버 /text 표 조각 줄 차례) · 값 칸 굵게 - 줄 누르면 그 줄 값으로 · 조건마다 「설계 때 정함 / 이 값으로 고정」 · 범위 칸은 범위규칙대로 안쪽 수 - 바뀐 식으로 이 줄 금액 · 합계 미리 보임(/text · /calc) · 고른 줄이 안 걸리면 [이걸로] 막음 - 시험 test_m01_easycalc_table.py 6 통과 · typecheck 통과 - ORCA: GF000219 석공 줄 35 · 깬잡석 고정 → 87,395 원(서버 87,395.1) · 합계 132,133 · GC000129 범위 줄 5~8 → 나무높이 6.5 · 541,940 → 709,080 · 값 칸 · 취소 · 닫기 null Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
"""쉬운 단가산출 2단계 — 표 보고 고르기(`M01_EasyCalc_Table.ts`)의 식 읽기 · 새 식 짜기.
|
|
|
|
화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 화면 부품 · 서버는 빈 흉내(순수 함수만 부름).
|
|
견본 식은 이 시험 안에 둠(진짜 마스터 내용에 기대지 않음).
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SOURCE = ROOT / "M01_MasterData" / "EasyCalc" / "M01_EasyCalc_Table.ts"
|
|
|
|
RUNNER = r"""
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const [root, file] = process.argv.slice(1);
|
|
const ts = require(path.join(root, "config", "node_modules", "typescript"));
|
|
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const stub = new Proxy({}, { get: () => () => undefined });
|
|
const box = { exports: {} };
|
|
new Function("exports", "module", "require", js)(box.exports, box, () => stub);
|
|
const m = box.exports;
|
|
const jobs = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
const out = jobs.map((job) => {
|
|
if (job.op === "read") return m.readFinds(job.formula);
|
|
if (job.op === "swap") {
|
|
const calls = m.readFinds(job.formula);
|
|
const swaps = calls.map((c, i) => {
|
|
const s = job.swaps[i];
|
|
return s ? m.writeFind(c.table, s.args ?? c.args, s.col ?? c.col) : null;
|
|
});
|
|
return m.replaceFinds(job.formula, calls, swaps);
|
|
}
|
|
if (job.op === "literal") return m.literal(job.value, job.rule ?? "");
|
|
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(SOURCE)],
|
|
input=json.dumps(jobs, ensure_ascii=False),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
check=True,
|
|
)
|
|
return json.loads(done.stdout)
|
|
|
|
|
|
MESSANGI = "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공 * (1 + 증가율 / 100)"
|
|
|
|
|
|
def test_식_읽기_흔한_꼴():
|
|
[calls] = run([{"op": "read", "formula": MESSANGI}])
|
|
assert len(calls) == 1
|
|
c = calls[0]
|
|
assert c["table"] == "QF000421" and c["col"] == "석공"
|
|
assert [(a["key"], a["rhs"]) for a in c["args"]] == [
|
|
("뒷길이", "뒷길이"),
|
|
("돌", "돌"),
|
|
("쌓기", "쌓기"),
|
|
]
|
|
assert MESSANGI[c["start"] : c["end"]] == "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공"
|
|
|
|
|
|
def test_식_읽기_따옴표_쉼표_괄호_겹침():
|
|
f = (
|
|
"(찾기(QC000524, 형식=형식).특별인부 - 만약(정밀절단 == '아님', "
|
|
"찾기(QC000583, 구분='가, 나 (다)').값, 0)) / 시공량"
|
|
)
|
|
[calls] = run([{"op": "read", "formula": f}])
|
|
assert [c["table"] for c in calls] == ["QC000524", "QC000583"]
|
|
assert calls[1]["args"] == [{"key": "구분", "rhs": "'가, 나 (다)'"}]
|
|
assert calls[1]["col"] == "값"
|
|
|
|
|
|
def test_식_읽기_못_읽는_꼴과_없는_꼴():
|
|
out = run(
|
|
[
|
|
{"op": "read", "formula": "찾기(QF:{지역}, 구분='a').값"}, # {자리} 낀 키
|
|
{"op": "read", "formula": "찾기(QF000001, 구분='a'"}, # 안 닫힘
|
|
{"op": "read", "formula": "품 * 1.2"},
|
|
{"op": "read", "formula": "'찾기(QF000001, 구분=1).값'"}, # 따옴표 글 안
|
|
]
|
|
)
|
|
assert out[0] is None and out[1] is None
|
|
assert out[2] == [] and out[3] == []
|
|
|
|
|
|
def test_새_식_짜기_줄_고정과_값칸_바꾸기():
|
|
[new] = run(
|
|
[
|
|
{
|
|
"op": "swap",
|
|
"formula": MESSANGI,
|
|
"swaps": [
|
|
{
|
|
"args": [
|
|
{"key": "뒷길이", "rhs": "35"},
|
|
{"key": "돌", "rhs": "'깬잡석'"},
|
|
{"key": "쌓기", "rhs": "쌓기"},
|
|
],
|
|
"col": "보통인부",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
)
|
|
assert new == (
|
|
"찾기(QF000421, 뒷길이=35, 돌='깬잡석', 쌓기=쌓기).보통인부 * (1 + 증가율 / 100)"
|
|
)
|
|
|
|
|
|
def test_새_식_짜기_둘째_찾기만_바꿈_나머지_글_그대로():
|
|
f = "찾기(QC1, 구분='가새').수량 * 찾기(QC2, 공기=공기).손율 / 100"
|
|
[new] = run(
|
|
[
|
|
{
|
|
"op": "swap",
|
|
"formula": f,
|
|
"swaps": [None, {"args": [{"key": "공기", "rhs": "'1개월'"}]}],
|
|
}
|
|
]
|
|
)
|
|
assert new == "찾기(QC1, 구분='가새').수량 * 찾기(QC2, 공기='1개월').손율 / 100"
|
|
# 다시 읽어도 같은 모양(왕복)
|
|
[calls] = run([{"op": "read", "formula": new}])
|
|
assert calls[1]["args"] == [{"key": "공기", "rhs": "'1개월'"}]
|
|
|
|
|
|
def test_칸_값을_식_글로():
|
|
out = run(
|
|
[
|
|
{"op": "literal", "value": 25},
|
|
{"op": "literal", "value": "깬잡석"},
|
|
{"op": "literal", "value": "it's"}, # 따옴표 든 글은 못 씀
|
|
{"op": "literal", "value": [20, 40], "rule": "초과·이하"},
|
|
{"op": "literal", "value": [None, 60], "rule": "이하"},
|
|
{"op": "literal", "value": [None, 60], "rule": "이상·미만"},
|
|
{"op": "literal", "value": [80, None], "rule": "초과·이하"},
|
|
{"op": "literal", "value": [80, None], "rule": "이상·미만"},
|
|
]
|
|
)
|
|
assert out == ["25", "'깬잡석'", None, "30", "60", "59", "81", "80"]
|