420 lines
10 KiB
JavaScript
420 lines
10 KiB
JavaScript
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),
|
|
},
|
|
root: {
|
|
category: 'Arithmetic',
|
|
argCount: 2,
|
|
aliases: ['y√x', 'yroot', 'nroot'],
|
|
execute: (calc, a, b) => {
|
|
if (b === 0) {
|
|
throw new Error('Invalid input for root');
|
|
}
|
|
if (a < 0 && b % 2 === 0) {
|
|
throw new Error('Invalid input for root');
|
|
}
|
|
return Math.pow(a, 1 / 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', 'root', '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.slice(0, argCount).reverse() : [];
|
|
const result = operation.execute(this, ...operands, ...args);
|
|
|
|
if (argCount > 0) {
|
|
this.stack.splice(0, argCount);
|
|
}
|
|
|
|
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;
|
|
}
|