feat: add dynamic constant management to the calculator core

This commit is contained in:
2026-05-16 02:23:20 +02:00
parent 2504716c64
commit 534bbc0afb
4 changed files with 83 additions and 20 deletions
+9 -8
View File
@@ -1,8 +1,9 @@
# Project memory # State
- Core engine: `src/rpn-calculator.js`
- RPN calculator JS project. - Active demo: `samples/calc-02/` portrait-only HP48GX layout; display-adjacent button row stays in 4 columns and now shares the same base button styling as the function keys
- Read `.memory/state.md` for current state. - Mode button shows the current angle mode only; selecting a mode uses a popup menu
- Keep names and commands in English. - Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant`
- Update memory files based on events: engine, demo, API, commands, exports, docs, or tasks. - Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Core arithmetic now includes `root` for y-th roots, and `samples/calc-02/` uses it for `y√x`. - Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic now includes `root` for y-th roots; constants can now be added or removed dynamically through the core API and the calc-02 constant menu reads from the engine
- `samples/calc-02/` in portrait mode remains the active responsive demo. - Demo actions: keyboard focus is kept on the hidden input on desktop so typing keeps working; the keypad layout places Enter in the bottom-left, ± in the former Enter position, and Esc before Clear for safety; paste parses clipboard text as a number before pushing it to the stack; Ctrl+V is supported via the hidden input paste event; backspace is ignored when the stack is empty; operation errors are shown as an overlay bar on top of the calculator with a shorter timeout and darker red; the display button row uses a 4-column grid with a spacer cell to preserve alignment
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+2 -2
View File
@@ -2,8 +2,8 @@
- Core engine: `src/rpn-calculator.js` - Core engine: `src/rpn-calculator.js`
- Active demo: `samples/calc-02/` portrait-only HP48GX layout; display-adjacent button row stays in 4 columns and now shares the same base button styling as the function keys - Active demo: `samples/calc-02/` portrait-only HP48GX layout; display-adjacent button row stays in 4 columns and now shares the same base button styling as the function keys
- Mode button shows the current angle mode only; selecting a mode uses a popup menu - Mode button shows the current angle mode only; selecting a mode uses a popup menu
- Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants` - Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant`
- Config: `maxSize`, `base`, `angleMode`, `enabledCommands` - Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic now includes `root` for y-th roots - Commands: arithmetic, stack, trigonometry, constants `pi`, `e`, `phi`, `g`, and `c`; arithmetic now includes `root` for y-th roots; constants can now be added or removed dynamically through the core API
- Demo actions: keyboard focus is kept on the hidden input on desktop so typing keeps working; the keypad layout places Enter in the bottom-left, ± in the former Enter position, and Esc before Clear for safety; paste parses clipboard text as a number before pushing it to the stack; Ctrl+V is supported via the hidden input paste event; backspace is ignored when the stack is empty; operation errors are shown as an overlay bar on top of the calculator with a shorter timeout and darker red; the display button row uses a 4-column grid with a spacer cell to preserve alignment - Demo actions: keyboard focus is kept on the hidden input on desktop so typing keeps working; the keypad layout places Enter in the bottom-left, ± in the former Enter position, and Esc before Clear for safety; paste parses clipboard text as a number before pushing it to the stack; Ctrl+V is supported via the hidden input paste event; backspace is ignored when the stack is empty; operation errors are shown as an overlay bar on top of the calculator with a shorter timeout and darker red; the display button row uses a 4-column grid with a spacer cell to preserve alignment
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports` - Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+17 -10
View File
@@ -383,6 +383,7 @@ modeButton.addEventListener('click', (event) => {
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
closeModeMenu(); closeModeMenu();
closeConstMenu(); closeConstMenu();
render();
}); });
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
closeModeMenu(); closeModeMenu();
@@ -422,13 +423,14 @@ hiddenInput.addEventListener('paste', (event) => {
upButton.addEventListener('click', () => {}); upButton.addEventListener('click', () => {});
const constants = [ const constantLabels = {
{ label: 'π', value: Math.PI }, pi: 'π',
{ label: 'e', value: Math.E }, e: 'e',
{ label: 'φ', value: (1 + Math.sqrt(5)) / 2 }, phi: 'φ',
{ label: 'g', value: 9.80665 }, g: 'g',
{ label: 'c', value: 299792458 }, c: 'c',
]; };
const constantOrder = ['pi', 'e', 'phi', 'g', 'c'];
let constMenuEl = null; let constMenuEl = null;
@@ -449,14 +451,19 @@ function toggleConstMenu() {
constMenuEl = document.createElement('div'); constMenuEl = document.createElement('div');
constMenuEl.className = 'mode-menu'; constMenuEl.className = 'mode-menu';
constMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`; constMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
constants.forEach((constant) => { const availableConstants = calc.listConstants();
const keys = [...constantOrder, ...Object.keys(availableConstants).filter((name) => !constantOrder.includes(name))];
keys.forEach((name) => {
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
button.className = 'mode-menu-item'; button.className = 'mode-menu-item';
button.textContent = constant.label; if (!Object.prototype.hasOwnProperty.call(availableConstants, name)) {
return;
}
button.textContent = constantLabels[name] ?? name;
button.addEventListener('click', () => { button.addEventListener('click', () => {
pushEditingValueIfNeeded(); pushEditingValueIfNeeded();
calc.push(constant.value); calc.push(availableConstants[name]);
render(); render();
clearStatus(); clearStatus();
closeConstMenu(); closeConstMenu();
+55
View File
@@ -2,6 +2,9 @@ class RpnCalculator {
static CONSTANTS = { static CONSTANTS = {
pi: Math.PI, pi: Math.PI,
e: Math.E, e: Math.E,
phi: (1 + Math.sqrt(5)) / 2,
g: 9.80665,
c: 299792458,
}; };
static OPERATIONS = { static OPERATIONS = {
@@ -212,6 +215,15 @@ class RpnCalculator {
this.enabledCommands = new Set(selectedCommands.map((name) => this.normalizeCommandName(name)).filter((name) => RpnCalculator.OPERATIONS[name])); this.enabledCommands = new Set(selectedCommands.map((name) => this.normalizeCommandName(name)).filter((name) => RpnCalculator.OPERATIONS[name]));
} }
static isValidConstantName(name) {
return typeof name === 'string' && name.trim() !== '';
}
static isReservedName(name) {
const normalized = typeof name === 'string' ? name.toLowerCase() : '';
return Boolean(RpnCalculator.OPERATIONS[normalized]);
}
toRadians(value) { toRadians(value) {
if (this.angleMode === 'grad') { if (this.angleMode === 'grad') {
return (value * Math.PI) / 200; return (value * Math.PI) / 200;
@@ -257,6 +269,45 @@ class RpnCalculator {
return typeof name === 'string' && Object.prototype.hasOwnProperty.call(this.constants, name.toLowerCase()); return typeof name === 'string' && Object.prototype.hasOwnProperty.call(this.constants, name.toLowerCase());
} }
setConstant(name, value) {
if (!RpnCalculator.isValidConstantName(name)) {
throw new Error('Invalid constant name');
}
if (!Number.isFinite(value)) {
throw new Error('Invalid constant value');
}
const normalized = name.trim().toLowerCase();
if (RpnCalculator.isReservedName(normalized)) {
throw new Error(`Constant name conflicts with a command: ${name}`);
}
this.constants[normalized] = value;
return value;
}
removeConstant(name) {
if (!RpnCalculator.isValidConstantName(name)) {
throw new Error('Invalid constant name');
}
const normalized = name.trim().toLowerCase();
if (!this.isConstantName(normalized)) {
return false;
}
delete this.constants[normalized];
return true;
}
hasConstant(name) {
if (!RpnCalculator.isValidConstantName(name)) {
return false;
}
return this.isConstantName(name.trim().toLowerCase());
}
push(value) { push(value) {
if (this.stack.length >= this.maxSize) { if (this.stack.length >= this.maxSize) {
throw new Error('Stack overflow'); throw new Error('Stack overflow');
@@ -408,6 +459,10 @@ class RpnCalculator {
getConstants() { getConstants() {
return { ...this.constants }; return { ...this.constants };
} }
listConstants() {
return this.getConstants();
}
} }
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {