Files

296 lines
11 KiB
JavaScript

"use strict";
/* JS-порт движка калькулятора: токенизация -> AST -> eval.
Правила и тексты ошибок — как в Python parser.py/nodes.py. */
const Calc = (() => {
class CalcError extends Error {
constructor(message, pos) {
super(message);
this.name = "CalcError";
this.pos = pos == null ? null : pos;
}
}
const CONSTANTS = { pi: Math.PI, e: Math.E, tau: Math.PI * 2, inf: Infinity };
const ARITIES = {
sqrt: [1], abs: [1], exp: [1], ln: [1], log: [1, 2], log2: [1], log10: [1],
round: [1, 2], floor: [1], ceil: [1], sin: [1], cos: [1], tan: [1],
asin: [1], acos: [1], atan: [1],
};
// Банковское округление (как Python round): половина — к чётному.
function roundHalfEven(x, nd) {
const p = Math.pow(10, nd || 0);
const y = x * p;
const f = Math.floor(y);
const d = y - f;
let r;
if (d > 0.5) r = f + 1;
else if (d < 0.5) r = f;
else r = (f % 2 === 0) ? f : f + 1;
return r / p;
}
function applyFn(name, args) {
const x = args[0];
switch (name) {
case "sqrt":
if (x < 0) throw new CalcError("Доменная ошибка функции: sqrt от отрицательного");
return Math.sqrt(x);
case "abs": return Math.abs(x);
case "exp": return Math.exp(x);
case "ln":
if (x <= 0) throw new CalcError("Доменная ошибка функции: ln");
return Math.log(x);
case "log": {
if (x <= 0) throw new CalcError("Доменная ошибка функции: log");
const base = args.length === 2 ? args[1] : Math.E;
if (base <= 0 || base === 1) throw new CalcError("Доменная ошибка функции: log (основание)");
return Math.log(x) / Math.log(base);
}
case "log2":
if (x <= 0) throw new CalcError("Доменная ошибка функции: log2");
return Math.log2(x);
case "log10":
if (x <= 0) throw new CalcError("Доменная ошибка функции: log10");
return Math.log10(x);
case "round":
return roundHalfEven(x, args.length === 2 ? Math.trunc(args[1]) : 0);
case "floor": return Math.floor(x);
case "ceil": return Math.ceil(x);
case "sin": return Math.sin(x);
case "cos": return Math.cos(x);
case "tan": return Math.tan(x);
case "asin":
if (x < -1 || x > 1) throw new CalcError("Доменная ошибка функции: asin");
return Math.asin(x);
case "acos":
if (x < -1 || x > 1) throw new CalcError("Доменная ошибка функции: acos");
return Math.acos(x);
case "atan": return Math.atan(x);
default: throw new CalcError(`Неизвестная функция: '${name}'`);
}
}
const NUM_RE = /^[0-9][0-9_]*(?:\.[0-9_]*)?(?:[eE][+-]?[0-9]+)?|^\.[0-9][0-9_]*(?:[eE][+-]?[0-9]+)?/;
const ID_START = /[a-zA-Z_]/, ID_CHAR = /[a-zA-Z0-9_]/;
function tokenize(src) {
const toks = [];
let i = 0;
while (i < src.length) {
const c = src[i];
if (c === " " || c === "\t" || c === "\n") { i++; continue; }
if ((c >= "0" && c <= "9") || c === ".") {
const m = NUM_RE.exec(src.slice(i));
if (!m) throw new CalcError(`Недопустимый символ: '${c}' (позиция ${i + 1})`, i);
toks.push({ t: "NUM", v: parseFloat(m[0].replace(/_/g, "")), p: i });
i += m[0].length;
continue;
}
if (ID_START.test(c)) {
let j = i;
while (j < src.length && ID_CHAR.test(src[j])) j++;
toks.push({ t: "ID", v: src.slice(i, j).toLowerCase(), p: i });
i = j;
continue;
}
if (c === "*" && src[i + 1] === "*") { toks.push({ t: "**", v: "**", p: i }); i += 2; continue; }
if (c === "/" && src[i + 1] === "/") { toks.push({ t: "//", v: "//", p: i }); i += 2; continue; }
if ("+-*/%".includes(c)) { toks.push({ t: "OP", v: c, p: i }); i++; continue; }
if (c === "(") { toks.push({ t: "LPAREN", v: "(", p: i }); i++; continue; }
if (c === ")") { toks.push({ t: "RPAREN", v: ")", p: i }); i++; continue; }
if (c === ",") { toks.push({ t: "COMMA", v: ",", p: i }); i++; continue; }
throw new CalcError(`Недопустимый символ: '${c}' (позиция ${i + 1})`, i);
}
return toks;
}
class Parser {
constructor(toks) { this.toks = toks; this.pos = 0; }
_peek() { return this.toks[this.pos] || null; }
_adv() {
const t = this._peek();
if (!t) throw new CalcError("Неожиданный конец выражения");
this.pos++;
return t;
}
_startsOperand(t) { return !!t && (t.t === "NUM" || t.t === "LPAREN" || t.t === "ID"); }
parse() {
if (!this.toks.length) throw new CalcError("Пустое выражение");
const node = this._expr();
if (this.pos < this.toks.length) throw new CalcError("Лишние токены в выражении");
return node;
}
_expr() {
let left = this._term();
for (;;) {
const t = this._peek();
if (t && t.t === "OP" && (t.v === "+" || t.v === "-")) {
this.pos++;
left = { k: "bin", op: t.v, l: left, r: this._term() };
} else break;
}
return left;
}
_term() {
let left = this._factor();
for (;;) {
const t = this._peek();
if (!t) break;
if (t.t === "OP" && (t.v === "*" || t.v === "/")) {
this.pos++; left = { k: "bin", op: t.v, l: left, r: this._factor() }; continue;
}
if (t.t === "//") {
this.pos++; left = { k: "bin", op: "//", l: left, r: this._factor() }; continue;
}
// '%': бинарный остаток, только если далее начинается операнд
if (t.t === "OP" && t.v === "%" && this._startsOperand(this.toks[this.pos + 1])) {
this.pos++; left = { k: "bin", op: "%", l: left, r: this._factor() }; continue;
}
break;
}
return left;
}
_factor() {
let base;
const t = this._peek();
if (t && t.t === "OP" && (t.v === "+" || t.v === "-")) {
this.pos++;
const operand = this._factor();
base = t.v === "-" ? { k: "un", x: operand } : operand;
} else {
base = this._postfix();
}
const nx = this._peek();
if (nx && nx.t === "**") {
this.pos++;
return { k: "pow", b: base, x: this._factor() }; // правосторонняя
}
return base;
}
_postfix() {
let node = this._atom();
for (;;) {
const t = this._peek();
if (t && t.t === "OP" && t.v === "%" && !this._startsOperand(this.toks[this.pos + 1])) {
this.pos++;
node = { k: "pct", x: node };
continue;
}
break;
}
return node;
}
_atom() {
const t = this._adv();
if (t.t === "NUM") return { k: "num", v: t.v };
if (t.t === "ID") {
const name = t.v;
const nx = this._peek();
if (nx && nx.t === "LPAREN") return this._call(name);
if (ARITIES[name]) throw new CalcError(`Функция '${name}' требует скобок: ${name}(...)`);
return { k: "id", name };
}
if (t.t === "LPAREN") {
const node = this._expr();
const close = this._peek();
if (!close || close.t !== "RPAREN") throw new CalcError("Не хватает закрывающей скобки ')'");
this.pos++;
return node;
}
if (t.t === "OP") throw new CalcError("Ожидалось число, константа, функция или скобку");
if (t.t === "**" || t.t === "//") throw new CalcError("Ожидалось число, константа, функция или скобку");
throw new CalcError(`Лишние токены в выражении`);
}
_call(name) {
if (!ARITIES[name]) throw new CalcError(`Неизвестная функция: '${name}'`);
this.pos++; // съедаем '('
const args = [];
const t = this._peek();
if (t && t.t === "RPAREN") {
this.pos++;
throw new CalcError(`'${name}' требует аргументов`);
}
for (;;) {
args.push(this._expr());
const c = this._peek();
if (c && c.t === "COMMA") { this.pos++; continue; }
break;
}
const close = this._peek();
if (!close || close.t !== "RPAREN") {
throw new CalcError(`Не хватает закрывающей скобки ')' в '${name}'`);
}
this.pos++;
if (!ARITIES[name].includes(args.length)) {
throw new CalcError(`'${name}': ожидалось аргументов ${ARITIES[name].join("/")}, получено ${args.length}`);
}
return { k: "call", name, args };
}
}
function _eval(node, env) {
switch (node.k) {
case "num": return node.v;
case "un": return -_eval(node.x, env);
case "bin": {
const a = _eval(node.l, env), b = _eval(node.r, env);
switch (node.op) {
case "+": return a + b;
case "-": return a - b;
case "*": return a * b;
case "/":
if (b === 0) throw new CalcError("Деление на ноль");
return a / b;
case "//":
if (b === 0) throw new CalcError("Деление на ноль");
return Math.floor(a / b);
case "%":
if (b === 0) throw new CalcError("Деление на ноль");
return ((a % b) + b) % b; // floor-остаток как в Python
}
throw new CalcError("Неизвестный оператор");
}
case "pow": {
const a = _eval(node.b, env), b = _eval(node.x, env);
const r = Math.pow(a, b);
if (Number.isNaN(r)) throw new CalcError("Недопустимая степень (математическая доменная ошибка)");
return r;
}
case "pct": return _eval(node.x, env) / 100;
case "call": return applyFn(node.name, node.args.map((a) => _eval(a, env)));
case "id": {
if (Object.prototype.hasOwnProperty.call(env, node.name)) return env[node.name];
if (node.name in CONSTANTS) return CONSTANTS[node.name];
throw new CalcError(`Неизвестный идентификатор: '${node.name}'`);
}
}
throw new CalcError("Неизвестный узел AST");
}
// Формат как Python ".12g": 12 значащих цифр, без хвостовых нулей.
function format(x) {
if (Number.isNaN(x)) return "nan";
if (!Number.isFinite(x)) return x > 0 ? "inf" : "-inf";
if (Number.isInteger(x) && Math.abs(x) < 1e21) return String(x);
return String(Number(x.toPrecision(12)));
}
function evaluate(expr, env) {
const ast = new Parser(tokenize(expr)).parse();
return _eval(ast, env || {});
}
return { CalcError, evaluate, format, CONSTANTS, ARITIES };
})();
if (typeof module !== "undefined" && module.exports) module.exports = Calc;