"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;