Files
Aislo/tools/est-test/native-formula.js

115 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 수식은 실행 가능한 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 };
})();