class RpnCalculator { static CONSTANTS = { pi: Math.PI, e: Math.E, }; static OPERATIONS = { add: { category: 'Arithmetic', argCount: 2, aliases: ['+'], execute: (calc, a, b) => a + b, }, sub: { category: 'Arithmetic', argCount: 2, aliases: ['-'], execute: (calc, a, b) => a - b, }, mul: { category: 'Arithmetic', argCount: 2, aliases: ['*'], execute: (calc, a, b) => a * b, }, div: { category: 'Arithmetic', argCount: 2, aliases: ['/'], execute: (calc, a, b) => { if (b === 0) { throw new Error('Division by zero'); } return a / b; }, }, mod: { category: 'Arithmetic', argCount: 2, aliases: ['%'], execute: (calc, a, b) => (a * b) / 100, }, pow: { category: 'Arithmetic', argCount: 2, aliases: ['^', 'y^x'], execute: (calc, a, b) => Math.pow(a, b), }, sqr: { category: 'Arithmetic', argCount: 1, execute: (calc, a) => a * a, }, neg: { category: 'Arithmetic', argCount: 1, execute: (calc, a) => -a, }, sqrt: { category: 'Arithmetic', argCount: 1, aliases: ['sqrt(x)'], execute: (calc, value) => { if (value < 0) { throw new Error('Invalid input for sqrt'); } return Math.sqrt(value); }, }, recip: { category: 'Arithmetic', argCount: 1, aliases: ['1/x'], execute: (calc, a) => { if (a === 0) { throw new Error('Division by zero'); } return 1 / a; }, }, sin: { category: 'Trigonometry', argCount: 1, execute: (calc, a) => Math.sin(calc.toRadians(a)), }, cos: { category: 'Trigonometry', argCount: 1, execute: (calc, a) => Math.cos(calc.toRadians(a)), }, tan: { category: 'Trigonometry', argCount: 1, execute: (calc, a) => Math.tan(calc.toRadians(a)), }, asin: { category: 'Trigonometry', argCount: 1, execute: (calc, a) => { if (a < -1 || a > 1) { throw new Error('Invalid input for asin'); } return calc.toDegrees(Math.asin(a)); }, }, acos: { category: 'Trigonometry', argCount: 1, execute: (calc, a) => { if (a < -1 || a > 1) { throw new Error('Invalid input for acos'); } return calc.toDegrees(Math.acos(a)); }, }, atan: { category: 'Trigonometry', argCount: 1, execute: (calc, a) => calc.toDegrees(Math.atan(a)), }, log: { category: 'Arithmetic', argCount: 1, execute: (calc, a) => { if (a <= 0) { throw new Error('Invalid input for log'); } return Math.log10(a); }, }, ln: { category: 'Arithmetic', argCount: 1, execute: (calc, a) => { if (a <= 0) { throw new Error('Invalid input for ln'); } return Math.log(a); }, }, dup: { category: 'Stack', argCount: 1, execute: (calc, a) => [a, a], }, drop: { category: 'Stack', argCount: 1, execute: () => [], }, swap: { category: 'Stack', argCount: 2, execute: (calc, a, b) => [b, a], }, clear: { category: 'Stack', argCount: 0, execute: (calc) => { calc.stack.length = 0; return []; }, }, enter: { category: 'Stack', argCount: 0, execute: (calc) => { calc.commitInput(); return []; }, }, }; static getOperationCategories() { return ['Stack', 'Arithmetic', 'Trigonometry']; } static getOperationsByCategory() { return { Stack: ['dup', 'drop', 'swap', 'clear', 'enter'], Arithmetic: ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'sqr', 'neg', 'sqrt', 'recip', 'log', 'ln'], Trigonometry: ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'], }; } constructor(options = {}) { this.maxSize = Number.isInteger(options.maxSize) && options.maxSize > 0 ? options.maxSize : 2048; this.stack = []; this.inputValue = ''; this.isEditing = false; this.base = Number.isInteger(options.base) && options.base >= 2 && options.base <= 16 ? options.base : 10; this.angleMode = ['deg', 'rad', 'grad'].includes(options.angleMode) ? options.angleMode : 'deg'; this.constants = { ...RpnCalculator.CONSTANTS }; const enabled = options.enabledCommands; const defaultCommands = Object.keys(RpnCalculator.OPERATIONS); const selectedCommands = Array.isArray(enabled) && enabled.length > 0 ? enabled : defaultCommands; this.enabledCommands = new Set(selectedCommands.map((name) => this.normalizeCommandName(name)).filter((name) => RpnCalculator.OPERATIONS[name])); } toRadians(value) { if (this.angleMode === 'grad') { return (value * Math.PI) / 200; } return this.angleMode === 'deg' ? (value * Math.PI) / 180 : value; } toDegrees(value) { if (this.angleMode === 'grad') { return (value * 200) / Math.PI; } return this.angleMode === 'deg' ? (value * 180) / Math.PI : value; } roundResult(value) { if (!Number.isFinite(value)) { return value; } const rounded = Math.round((value + Number.EPSILON) * 1e12) / 1e12; return Object.is(rounded, -0) ? 0 : rounded; } formatNumber(value) { if (!Number.isFinite(value)) { return String(value); } const normalized = this.roundResult(value); return Number.isInteger(normalized) ? String(normalized) : normalized.toFixed(12).replace(/\.0+$/, '').replace(/0+$/, '').replace(/\.$/, ''); } normalizeCommandName(name) { if (typeof name !== 'string') return ''; const lower = name.toLowerCase(); for (const [key, def] of Object.entries(RpnCalculator.OPERATIONS)) { if (key === lower || (def.aliases && def.aliases.includes(lower))) return key; } return lower; } isConstantName(name) { return typeof name === 'string' && Object.prototype.hasOwnProperty.call(this.constants, name.toLowerCase()); } push(value) { if (this.stack.length >= this.maxSize) { throw new Error('Stack overflow'); } this.stack.unshift(value); return value; } pop() { if (this.stack.length === 0) { throw new Error('Stack underflow'); } return this.stack.shift(); } clear() { this.stack.length = 0; this.inputValue = ''; this.isEditing = false; } swap(index1, index2) { if (!this.isValidIndex(index1) || !this.isValidIndex(index2)) { throw new Error('Invalid stack index'); } const temp = this.stack[index1]; this.stack[index1] = this.stack[index2]; this.stack[index2] = temp; } remove(index) { if (!this.isValidIndex(index)) { throw new Error('Invalid stack index'); } this.stack.splice(index, 1); } edit(index) { if (!this.isValidIndex(index)) { throw new Error('Invalid stack index'); } this.inputValue = this.formatNumber(this.stack[index]); this.isEditing = true; return this.inputValue; } isValidIndex(index) { return Number.isInteger(index) && index >= 0 && index < this.stack.length; } commitInput() { if (!this.isEditing || this.inputValue === '') { this.isEditing = false; return null; } const value = this.parseInputValue(this.inputValue); this.push(value); this.inputValue = ''; this.isEditing = false; return value; } parseInputValue(value) { if (typeof value !== 'string' || value.trim() === '') { throw new Error('Invalid input value'); } const parsed = this.base === 10 ? Number(value) : parseInt(value, this.base); if (Number.isNaN(parsed)) { throw new Error('Invalid number'); } return parsed; } input(command) { if (typeof command !== 'string') { throw new Error('Command must be a string'); } const trimmed = command.trim(); if (trimmed === '') return null; if (this.isInputCharacter(trimmed)) { this.isEditing = true; this.inputValue += trimmed; return this.inputValue; } return this.command(trimmed); } isInputCharacter(value) { if (value.length !== 1) return false; const allowed = '0123456789'; const letters = 'ABCDEFabcdef'; const sign = '+-'; const decimal = '.'; return allowed.includes(value) || letters.includes(value) || sign.includes(value) || decimal === value; } command(name, ...args) { const normalized = this.normalizeCommandName(name); if (this.isConstantName(normalized)) { if (this.isEditing) { this.commitInput(); } return this.push(this.constants[normalized]); } const operation = RpnCalculator.OPERATIONS[normalized]; if (!operation) { throw new Error(`Unknown command: ${name}`); } if (!this.enabledCommands.has(normalized)) { throw new Error(`Command disabled: ${name}`); } if (this.isEditing) { this.commitInput(); } const argCount = operation.argCount ?? 0; if (this.stack.length < argCount) { throw new Error('Stack underflow'); } const operands = argCount > 0 ? this.stack.splice(0, argCount).reverse() : []; const result = operation.execute(this, ...operands, ...args); if (Array.isArray(result)) { for (let i = result.length - 1; i >= 0; i -= 1) { this.push(result[i]); } } else if (result !== undefined) { this.push(result); } return result; } getOperationsByCategory() { return RpnCalculator.getOperationsByCategory(); } getConstants() { return { ...this.constants }; } } if (typeof window !== 'undefined') { window.RpnCalculator = RpnCalculator; } if (typeof module !== 'undefined' && module.exports) { module.exports = RpnCalculator; }