feat: add browser RPN calculator engine and demo
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
# Project rules — RPN Virtual Calculator
|
||||||
|
|
||||||
|
- Build a browser-friendly RPN calculator as a JavaScript class, preferably in a single file.
|
||||||
|
- Constructor options: `maxSize` (default 2048), `base` (default 10), `angleMode` (`deg` default; also `rad` and `grad`), `enabledCommands`.
|
||||||
|
- Available constants: `pi`, `e`.
|
||||||
|
- Public API is generic only: `push`, `pop`, `clear`, `swap(index1, index2)`, `remove(index)`, `edit(index)`, `isValidIndex(index)`, `input(command)`, and `command(name, ...args)`.
|
||||||
|
- Expose `inputValue` as a string and `isEditing` as a boolean.
|
||||||
|
- Supported operations are centralized in one dictionary with `argCount`, category, and aliases.
|
||||||
|
- Categories are limited to `Stack`, `Arithmetic`, and `Trigonometry`.
|
||||||
|
- Current commands: `add`, `sub`, `mul`, `div`, `mod`, `pow`, `sqr`, `neg`, `sqrt`, `recip`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `log`, `ln`, `dup`, `drop`, `swap`, `clear`, `enter`.
|
||||||
|
- Aliases: `+`, `-`, `*`, `/`, `%`, `^`, `y^x`, `1/x`.
|
||||||
|
- `mod` is the percentage operator: `a b % => (a * b) / 100`.
|
||||||
|
- `sqrt`, `asin`, `acos`, `log`, and `ln` must throw clear, explicit domain errors.
|
||||||
|
- `log` uses `Math.log10`; `ln` uses `Math.log`.
|
||||||
|
- Trig functions use degrees in the demo; `sin`, `cos`, `tan` convert to radians, inverse trig returns degrees.
|
||||||
|
- `inputValue` stays a string to keep hexadecimal input possible later.
|
||||||
|
- Example HTML must group buttons by `Stack`, `Arithmetic`, and `Trigonometry`, and call `command(...)`.
|
||||||
|
- Keep code names, categories, and API identifiers in English.
|
||||||
|
- Keep this file updated after each project change using the provided editing tools.
|
||||||
|
|
||||||
@@ -1,2 +1,123 @@
|
|||||||
# mtm-rpn-js
|
# RPN Virtual Calculator
|
||||||
|
|
||||||
|
A browser-friendly RPN calculator implemented as a single JavaScript class, with a simple API and an example HTML interface.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The project includes:
|
||||||
|
|
||||||
|
- `rpn-calculator.js`: calculator engine
|
||||||
|
- `rpn-example.html`: browser demo
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Self-contained JavaScript class
|
||||||
|
- Configurable stack size (`maxSize`, default: 2048)
|
||||||
|
- Configurable numeric base (`base`, default: 10)
|
||||||
|
- Configurable angle mode (`angleMode`, default: `deg`)
|
||||||
|
- Optional command enabling via `enabledCommands`
|
||||||
|
- Generic public API centered on `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, and `command`
|
||||||
|
- `inputValue` stays a string to keep hexadecimal input possible later
|
||||||
|
- `isEditing` tracks typed input mode
|
||||||
|
- Operations are centralized with `argCount`, category, and aliases
|
||||||
|
- Categories are limited to `Stack`, `Arithmetic`, and `Trigonometry`
|
||||||
|
- Clear domain errors for invalid inputs
|
||||||
|
- Degree-based trig demo in the example HTML
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
### In the browser
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="rpn-calculator.js"></script>
|
||||||
|
<script>
|
||||||
|
const calc = new RpnCalculator();
|
||||||
|
calc.input('1');
|
||||||
|
calc.input('2');
|
||||||
|
calc.command('enter');
|
||||||
|
calc.command('add');
|
||||||
|
console.log(calc.stack);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### With options
|
||||||
|
|
||||||
|
```js
|
||||||
|
const calc = new RpnCalculator({
|
||||||
|
maxSize: 1024,
|
||||||
|
base: 10,
|
||||||
|
angleMode: 'deg',
|
||||||
|
enabledCommands: ['add', 'sub', 'mul', 'div', 'enter', 'clear'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
- `stack`: current stack, with the top item at index `0`
|
||||||
|
- `inputValue`: input text as a string
|
||||||
|
- `isEditing`: whether input is currently being edited
|
||||||
|
|
||||||
|
### Generic methods
|
||||||
|
|
||||||
|
- `push(value)`
|
||||||
|
- `pop()`
|
||||||
|
- `clear()`
|
||||||
|
- `swap(index1, index2)`
|
||||||
|
- `remove(index)`
|
||||||
|
- `edit(index)`
|
||||||
|
- `isValidIndex(index)`
|
||||||
|
- `input(command)`
|
||||||
|
- `command(name, ...args)`
|
||||||
|
|
||||||
|
## Supported commands
|
||||||
|
|
||||||
|
### Stack
|
||||||
|
|
||||||
|
- `enter`
|
||||||
|
- `dup`
|
||||||
|
- `drop`
|
||||||
|
- `swap`
|
||||||
|
- `clear`
|
||||||
|
|
||||||
|
### Arithmetic
|
||||||
|
|
||||||
|
- `add` / `+`
|
||||||
|
- `sub` / `-`
|
||||||
|
- `mul` / `*`
|
||||||
|
- `div` / `/`
|
||||||
|
- `mod` / `%`
|
||||||
|
- `pow` / `^` / `y^x`
|
||||||
|
- `sqr`
|
||||||
|
- `neg`
|
||||||
|
- `sqrt`
|
||||||
|
- `recip` / `1/x`
|
||||||
|
- `log`
|
||||||
|
- `ln`
|
||||||
|
|
||||||
|
### Trigonometry
|
||||||
|
|
||||||
|
- `sin`
|
||||||
|
- `cos`
|
||||||
|
- `tan`
|
||||||
|
- `asin`
|
||||||
|
- `acos`
|
||||||
|
- `atan`
|
||||||
|
|
||||||
|
## Important behavior
|
||||||
|
|
||||||
|
- `%` behaves as the RPN percentage operator: `a b % => (a * b) / 100`
|
||||||
|
- `sqrt`, `asin`, `acos`, `log`, and `ln` throw clear errors for invalid inputs
|
||||||
|
- `log` uses `Math.log10`
|
||||||
|
- `ln` uses `Math.log`
|
||||||
|
- `sin`, `cos`, and `tan` convert degrees to radians in the default demo
|
||||||
|
- `asin`, `acos`, and `atan` return degrees in `deg` mode
|
||||||
|
|
||||||
|
## Example HTML
|
||||||
|
|
||||||
|
The example UI groups buttons into `Stack`, `Arithmetic`, and `Trigonometry` sections, and calls `command(...)` to execute operations.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
To be completed according to your project.
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>RPN Calculator Demo</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--bg: #f4f4f4;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--text: #111;
|
||||||
|
--muted: #666;
|
||||||
|
--button: #e9e9e9;
|
||||||
|
--button-text: #111;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--accent: #0a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #111;
|
||||||
|
--panel: #1a1a1a;
|
||||||
|
--text: #f3f3f3;
|
||||||
|
--muted: #aaa;
|
||||||
|
--button: #2a2a2a;
|
||||||
|
--button-text: #f3f3f3;
|
||||||
|
--border: #333;
|
||||||
|
--accent: #3dc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 24px auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display {
|
||||||
|
background: #000;
|
||||||
|
color: #0f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
min-height: 72px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, select, input {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--button);
|
||||||
|
color: var(--button-text);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 180px;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<div class="card">
|
||||||
|
<h1>RPN Calculator Demo</h1>
|
||||||
|
|
||||||
|
<div id="display" class="display"></div>
|
||||||
|
<div id="stack" class="stack"></div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>
|
||||||
|
<div class="section-title">Input</div>
|
||||||
|
<input id="input" type="text" placeholder="Type a number, pi, e, or a command, then press Enter" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<div class="section-title">Angle mode</div>
|
||||||
|
<select id="angleMode">
|
||||||
|
<option value="deg">Degrees</option>
|
||||||
|
<option value="rad">Radians</option>
|
||||||
|
<option value="grad">Grads</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<div class="badge">Mode: <span id="angleModeLabel" class="accent"></span></div>
|
||||||
|
<div class="badge">Base: <span id="baseLabel" class="accent"></span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">Constants</div>
|
||||||
|
<div class="grid">
|
||||||
|
<button data-const="pi">pi</button>
|
||||||
|
<button data-const="e">e</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">Stack</div>
|
||||||
|
<div class="grid">
|
||||||
|
<button data-cmd="enter">Enter</button>
|
||||||
|
<button data-cmd="dup">Dup</button>
|
||||||
|
<button data-cmd="swap">Swap top 2</button>
|
||||||
|
<button data-cmd="drop">Drop</button>
|
||||||
|
<button data-cmd="clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">Arithmetic</div>
|
||||||
|
<div class="grid">
|
||||||
|
<button data-cmd="add">+</button>
|
||||||
|
<button data-cmd="sub">−</button>
|
||||||
|
<button data-cmd="mul">×</button>
|
||||||
|
<button data-cmd="div">÷</button>
|
||||||
|
<button data-cmd="mod">%</button>
|
||||||
|
<button data-cmd="pow">y^x</button>
|
||||||
|
<button data-cmd="sqr">x²</button>
|
||||||
|
<button data-cmd="neg">±</button>
|
||||||
|
<button data-cmd="sqrt">sqrt</button>
|
||||||
|
<button data-cmd="recip">1/x</button>
|
||||||
|
<button data-cmd="log">log</button>
|
||||||
|
<button data-cmd="ln">ln</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">Trigonometry</div>
|
||||||
|
<div class="grid">
|
||||||
|
<button data-cmd="sin">sin</button>
|
||||||
|
<button data-cmd="cos">cos</button>
|
||||||
|
<button data-cmd="tan">tan</button>
|
||||||
|
<button data-cmd="asin">asin</button>
|
||||||
|
<button data-cmd="acos">acos</button>
|
||||||
|
<button data-cmd="atan">atan</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="muted">Tip: trig functions follow the selected angle mode. Domain errors are reported with clear messages. sqrt computes the square root of the top stack value.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../../src/rpn-calculator.js"></script>
|
||||||
|
<script>
|
||||||
|
const enabledCommands = ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'sqr', 'neg', 'sqrt', 'recip', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'log', 'ln', 'dup', 'swap', 'drop', 'clear', 'enter'];
|
||||||
|
const calc = new RpnCalculator({ angleMode: 'deg', enabledCommands });
|
||||||
|
|
||||||
|
const display = document.getElementById('display');
|
||||||
|
const stack = document.getElementById('stack');
|
||||||
|
const input = document.getElementById('input');
|
||||||
|
const angleMode = document.getElementById('angleMode');
|
||||||
|
const angleModeLabel = document.getElementById('angleModeLabel');
|
||||||
|
const baseLabel = document.getElementById('baseLabel');
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
display.textContent = calc.isEditing ? `Editing: ${calc.inputValue}` : 'Ready';
|
||||||
|
stack.innerHTML = calc.stack.length
|
||||||
|
? calc.stack.map((value, index) => `<div>${index}: ${value}</div>`).join('')
|
||||||
|
: '<span class="muted">Stack empty</span>';
|
||||||
|
angleMode.value = calc.angleMode;
|
||||||
|
angleModeLabel.textContent = calc.angleMode;
|
||||||
|
baseLabel.textContent = String(calc.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitInput() {
|
||||||
|
if (input.value.trim() !== '') {
|
||||||
|
calc.inputValue = input.value;
|
||||||
|
calc.isEditing = true;
|
||||||
|
calc.command('enter');
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(name) {
|
||||||
|
if (name === 'swap') {
|
||||||
|
if (calc.stack.length >= 2) calc.swap(0, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name === 'enter') {
|
||||||
|
commitInput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
calc.command(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('input', (event) => {
|
||||||
|
calc.inputValue = event.target.value;
|
||||||
|
calc.isEditing = event.target.value.length > 0;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
try {
|
||||||
|
commitInput();
|
||||||
|
render();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
angleMode.addEventListener('change', (event) => {
|
||||||
|
calc.angleMode = ['deg', 'rad', 'grad'].includes(event.target.value) ? event.target.value : 'deg';
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('button[data-cmd]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
runCommand(button.dataset.cmd);
|
||||||
|
render();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('button[data-const]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
calc.inputValue = '';
|
||||||
|
calc.isEditing = false;
|
||||||
|
calc.command(button.dataset.const);
|
||||||
|
input.value = '';
|
||||||
|
render();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user