/* -*- 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;