Files
Aislo/tools/stmate-test/scr_edit_expr.js
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면
- 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면
- 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림
- 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표
- 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0)
- DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인)
- 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 19:12:04 +09:00

324 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* -*- coding: utf-8 -*- */
/**
* 산출근거 식 평가기 (scr_edit_expr.js) — 사칙·괄호·함수·조건·대괄호 표시식 · 순수 계산
* 근거: REA 30 ST_EdSan TWM_EDIT_SAN cBoxFunc(∏ · INT · SIN · COS · TAN · SQRT · √ · ROUND ·
* ROUNDDOWN · CUT · LOG · LN · ABS · EXP · MOD · POWER · FACT · IIF · [2/3]) · Memo_Use 문법
* (3) round(A/B,4) · [?:4R] · [?:2U] · [?:0C] · (9) [Q=0.208] · 지식DB 11 §4.2(ROUND = RoundTo 짝수
* 반올림 · ROUNDDOWN = 0 방향) · 24 §2.3(IIF · IFI · IFO) · REA 18 IIF 인자 순서(참 = 두 번째)
* 연산 차례 — 곱·나눗셈은 왼쪽부터 차례로(18 §2.1) · 덧뺄셈보다 먼저 · 거듭제곱(^)은 오른쪽부터
*/
const StmateSmtExpr = (() => {
const EPS = 1e-9;
const trunc = (v, d = 0) => {
const f = 10 ** d,
s = v * f;
return Math.trunc(s + (s >= 0 ? EPS : -EPS)) / f;
};
// RoundTo — 정확히 절반이면 짝수 쪽 · 0 대칭
const roundEven = (v, d = 0) => {
const unit = d > 0 ? 1 / 10 ** d : 10 ** -d,
s = Math.abs(v) / unit,
fl = Math.floor(s),
diff = s - fl;
const r = diff > 0.5 || (diff === 0.5 && fl % 2 === 1) ? fl + 1 : fl;
const out = d > 0 ? r / 10 ** d : r * unit;
return v < 0 ? -out : out;
};
// 올림 — 절사 뒤 남은 끝이 있으면 한 단위 더(0에서 멀어짐)
const roundUp = (v, d = 0) => {
const t = trunc(v, d);
const f = 10 ** d;
return Math.abs(v - t) * f > EPS ? (Math.round(t * f) + Math.sign(v)) / f : t;
};
const fact = (n) => {
let r = 1;
for (let i = 2; i <= Math.trunc(n); i++) r *= i;
return r;
};
const pick = (c, a, b) => (c ? a : b);
const FUNCS = {
INT: [1, (x) => Math.trunc(x)],
ROUND: [[1, 2], (x, d = 0) => roundEven(x, d)],
ROUNDDOWN: [[1, 2], (x, d = 0) => trunc(x, d)],
CUT: [[1, 2], (x, d = 0) => trunc(x, d)],
ROUNDUP: [[1, 2], (x, d = 0) => roundUp(x, d)],
SQRT: [1, Math.sqrt],
SIN: [1, Math.sin],
COS: [1, Math.cos],
TAN: [1, Math.tan],
LOG: [2, (b, x) => (b === 10 ? Math.log10(x) : Math.log(x) / Math.log(b))],
LN: [1, Math.log],
ABS: [1, Math.abs],
EXP: [1, Math.exp],
MOD: [2, (a, b) => a - b * Math.trunc(a / b)],
POWER: [2, Math.pow],
FACT: [1, fact],
IIF: [3, pick],
IF: [3, pick],
IFI: [3, pick],
IFO: [3, pick],
};
const RND = { R: roundEven, U: roundUp, C: trunc };
function tokenize(src) {
const out = [];
const s = String(src);
let i = 0;
while (i < s.length) {
const c = s[i];
if (/\s/.test(c)) {
i++;
continue;
}
const num = s.slice(i).match(/^(\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/);
if (num) {
out.push({ t: "num", v: parseFloat(num[0]) });
i += num[0].length;
continue;
}
const id = s.slice(i).match(/^[A-Za-z_가-힣][A-Za-z_0-9가-힣]*/);
if (id) {
out.push({ t: "id", v: id[0] });
i += id[0].length;
continue;
}
const rnd = s.slice(i).match(/^:\s*(\d)\s*([RUCruc])(?=\s*\])/);
if (rnd) {
out.push({ t: "rnd", v: rnd[0], d: Number(rnd[1]), k: rnd[2].toUpperCase() });
i += rnd[0].length;
continue;
}
const two = s.slice(i, i + 2);
if ([">=", "<=", "<>", "!="].includes(two)) {
out.push({ t: "op", v: two === "!=" ? "<>" : two });
i += 2;
continue;
}
if ("+-*/^()<>=,√∏÷×[]".includes(c)) {
out.push({ t: "op", v: c === "÷" ? "/" : c === "×" ? "*" : c });
i++;
continue;
}
throw new Error(`알 수 없는 글자 「${c}」`);
}
return out;
}
/**
* 식 하나 평가 — vars: 대문자 이름 → 값 · 대괄호 [이름=식] 은 vars 에 씀
* 돌려줌: { value, error, missing: [정의 안 된 변수] }
*/
function evaluate(src, vars = {}) {
const missing = [];
let toks,
p = 0;
const peek = () => toks[p];
const isOp = (v) => toks[p] && toks[p].t === "op" && toks[p].v === v;
const expect = (v) => {
if (!isOp(v)) throw new Error(`「${v}」 빠짐`);
p++;
};
function cmp() {
let a = add();
while (
toks[p] &&
toks[p].t === "op" &&
[">", "<", ">=", "<=", "=", "<>"].includes(toks[p].v)
) {
const op = toks[p++].v,
b = add();
a =
op === ">"
? a > b
: op === "<"
? a < b
: op === ">="
? a >= b
: op === "<="
? a <= b
: op === "="
? Math.abs(a - b) < EPS
: Math.abs(a - b) >= EPS;
a = a ? 1 : 0;
}
return a;
}
function add() {
let a = mul();
while (isOp("+") || isOp("-")) {
const op = toks[p++].v,
b = mul();
a = op === "+" ? a + b : a - b;
}
return a;
}
function mul() {
let a = unary();
while (isOp("*") || isOp("/")) {
const op = toks[p++].v,
b = unary();
a = op === "*" ? a * b : a / b;
}
return a;
}
function unary() {
if (isOp("-")) return (p++, -unary());
if (isOp("+")) return (p++, unary());
return power();
}
function power() {
const a = primary();
if (isOp("^")) {
p++;
return Math.pow(a, unary());
}
return a;
}
function args() {
const out = [];
expect("(");
if (!isOp(")")) {
out.push(cmp());
while (isOp(",")) (p++, out.push(cmp()));
}
expect(")");
return out;
}
function primary() {
const t = peek();
if (!t) throw new Error("식이 끝남");
if (t.t === "num") return (p++, t.v);
if (t.t === "op" && t.v === "(") {
p++;
const v = cmp();
expect(")");
return v;
}
if (t.t === "op" && t.v === "√")
return (p++, Math.sqrt(isOp("(") ? (p++, ((v) => (expect(")"), v))(cmp())) : primary()));
if (t.t === "op" && t.v === "∏") return (p++, Math.PI);
if (t.t === "op" && t.v === "[") return (p++, bracket());
if (t.t === "id") {
p++;
const name = t.v.toUpperCase();
if (isOp("(")) {
const f = FUNCS[name];
if (!f) throw new Error(`지원하지 않는 함수 「${t.v}」`);
const a = args();
const n = f[0];
const ok = Array.isArray(n) ? a.length >= n[0] && a.length <= n[1] : a.length === n;
if (!ok) throw new Error(`「${t.v}」 인자 수 ${a.length}개 틀림`);
return f[1](...a);
}
if (name === "PI") return Math.PI;
if (vars[name] === undefined) {
missing.push(t.v);
return 0;
}
return Number(vars[name]) || 0;
}
throw new Error(`「${t.v}」 자리가 틀림`);
}
// [이름=식] 대입 · [식:자리R|U|C] 자리 처리 · [식] 값
function bracket() {
let name = null;
if (
toks[p] &&
toks[p].t === "id" &&
toks[p + 1] &&
toks[p + 1].t === "op" &&
toks[p + 1].v === "="
) {
name = toks[p].v.toUpperCase();
p += 2;
}
let v = cmp();
if (toks[p] && toks[p].t === "rnd") v = RND[toks[p].k](v, toks[p++].d);
expect("]");
if (name) vars[name] = v;
return v;
}
try {
toks = tokenize(src);
if (!toks.length) return { value: null, error: null, missing };
const value = cmp();
if (p < toks.length) throw new Error(`「${toks[p].v}」 뒤가 남음`);
if (!Number.isFinite(value)) return { value: 0, error: "계산 불가(0 나눔 등)", missing };
return { value, error: null, missing };
} catch (e) {
return { value: null, error: e.message, missing };
}
}
// 괄호·대괄호 밖의 구분자로 나눔
function splitTop(s, sep = ",") {
const out = [];
let depth = 0,
cur = "";
for (const c of String(s)) {
if (c === "(" || c === "[") depth++;
if (c === ")" || c === "]") depth--;
if (c === sep && depth === 0) {
out.push(cur);
cur = "";
} else cur += c;
}
out.push(cur);
return out;
}
// 괄호·대괄호 밖의 '=' 자리들(:= · <= · >= · <> 제외)
function topEqs(s) {
const at = [];
let depth = 0;
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (c === "(" || c === "[") depth++;
else if (c === ")" || c === "]") depth--;
else if (c === "=" && depth === 0 && !":<>".includes(s[i - 1] || " ")) at.push(i);
}
return at;
}
const lastTopEq = (s) => topEqs(s).pop() ?? -1;
const firstTopEq = (s) => topEqs(s)[0] ?? -1;
const FUNC_LIST = [
"∏",
"INT(0)",
"SIN(0)",
"COS(0)",
"TAN(0)",
"SQRT(1)",
"√(1)",
"ROUND(1,0)",
"ROUNDDOWN(1,0)",
"CUT(1,0)",
"LOG(10,1)",
"LN(1)",
"ABS(1)",
"EXP(1)",
"MOD(1,1)",
"POWER(1,1)",
"FACT(1)",
"IIF(1>0, 2, 3)",
";;",
"[2/3]",
];
return {
evaluate,
tokenize,
splitTop,
lastTopEq,
firstTopEq,
trunc,
roundEven,
roundUp,
RND,
FUNCS,
FUNC_LIST,
};
})();
if (typeof window !== "undefined") window.STMATE_SMT_EXPR = StmateSmtExpr;
if (typeof module !== "undefined" && module.exports) module.exports = StmateSmtExpr;