// 수식은 실행 가능한 JavaScript로 변환하지 않고 토큰과 구문 트리로 평가한다. const EstExpression = (() => { function evaluate(input, resolve = () => undefined, settings = {}) { const text = String(input) .replace(/×|·/g, "*") .replace(/÷/g, "/") .replace(/−|-/g, "-") .replace(/+/g, "+") .replace(/%/g, "%") .replace(/√\s*\(/g, "SQRT(") .replace(/π/g, "PI") .replace(/(?<=[\d\u0370-\u03ff])\s*(?=\()/g, "*") .replace(/(?<=[\d.)])\s*[xX]\s*(?=[\d.(A-Za-z])/g, "*") .replace(/(?<=[A-Z])x(?=[A-Z])/g, "*") .replace(/(?<=\d),(?=\d{3}(?:\D|$))/g, ""); const tokens = []; const pattern = /\s+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|\[[^\]]+\]|[A-Za-z_가-힣\u0370-\u03ff][A-Za-z_0-9가-힣\u0370-\u03ff.]*|[+*/^%(),:~=-]/y; let at = 0; while (at < text.length) { pattern.lastIndex = at; const match = pattern.exec(text); if (!match) throw Error(`수식 문자 오류 (${at + 1}): ${text.slice(at, at + 12)}`); if (match[0].trim()) tokens.push(match[0]); at = pattern.lastIndex; } let position = 0; const functions = { ABS: Math.abs, SQRT: Math.sqrt, MIN: Math.min, MAX: Math.max, ROUND: (n, d = 0) => EstMath.floating(n, d, 1), TRUNC: (n, d = 0) => EstMath.floating(n, d, 2), FLOOR: Math.floor, CEIL: Math.ceil, SUM: (...args) => args.reduce((a, b) => a + b, 0), }; function value(token, force = false) { const key = token.startsWith("[") ? token.slice(1, -1) : token; const resolved = resolve(key); const numeric = /^(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(key); if (resolved !== undefined && (!numeric || force || settings.numericReferences)) return Number(resolved); if (numeric && !force) return Number(key); if (key === "PI") return Math.PI; throw Error(`참조 항목 없음: ${key}`); } function atom() { const token = tokens[position++]; if (token === undefined) throw Error("수식이 끝나지 않았습니다."); if (token === "+" || token === "-") return (token === "-" ? -1 : 1) * atom(); let n; if (token === "(") { n = sum(); if (tokens[position++] !== ")") throw Error("괄호가 맞지 않습니다."); } else if (functions[token.toUpperCase()] && tokens[position] === "(") { position++; const args = []; if (tokens[position] !== ")") { args.push(sum()); while (tokens[position] === ",") { position++; args.push(sum()); } } if (tokens[position++] !== ")") throw Error("함수 괄호 오류"); n = functions[token.toUpperCase()](...args); } else if ([":", "~"].includes(tokens[position])) { position++; const end = tokens[position++]; if (!settings.range) throw Error("범위 참조를 사용할 수 없습니다."); n = settings.range(token.replace(/^\[|\]$/g, ""), end?.replace(/^\[|\]$/g, "")); } else n = value(token, token.startsWith("[")); while (tokens[position] === "%") { position++; n /= 100; } return n; } function power() { const n = atom(); if (tokens[position] === "^") { position++; return n ** power(); } return n; } function product() { let n = power(); while (["*", "/"].includes(tokens[position])) { const op = tokens[position++], v = power(); if (op === "/" && v === 0) throw Error("0으로 나눌 수 없습니다."); n = op === "*" ? n * v : n / v; } return n; } function sum() { let n = product(); while (["+", "-"].includes(tokens[position])) { const op = tokens[position++], v = product(); n = op === "+" ? n + v : n - v; } return n; } const result = sum(); if (position !== tokens.length || !Number.isFinite(result)) throw Error("수식 결과가 유효하지 않습니다."); return result; } return { evaluate }; })();