269 lines
8.9 KiB
JavaScript
269 lines
8.9 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;
|