- 보고서: 원본 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>
324 lines
9.3 KiB
JavaScript
324 lines
9.3 KiB
JavaScript
/* -*- 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;
|