feat(est): reproduce native tables and calculations

This commit is contained in:
2026-11-05 06:07:05 +09:00
parent 646eab6245
commit 60ccedcd9a
30 changed files with 5249 additions and 22 deletions
+110
View File
@@ -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 };
})();