115 lines
4.1 KiB
JavaScript
115 lines
4.1 KiB
JavaScript
// 수식은 실행 가능한 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 };
|
||
})();
|