feat(est): reproduce native tables and calculations
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// 수식은 실행 가능한 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(/(?<=\d),(?=\d{3}(?:\D|$))/g, "");
|
||||
const tokens = [];
|
||||
const pattern =
|
||||
/\s+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|\[[^\]]+\]|[A-Za-z_가-힣][A-Za-z_0-9가-힣.]*|[+*/^%(),:~=-]/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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user