feat: add dynamic constant management to the calculator core
This commit is contained in:
+9
-8
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
Reference in New Issue
Block a user