diff --git a/.memory/project.md b/.memory/project.md index 8189f7a..b0acf51 100644 --- a/.memory/project.md +++ b/.memory/project.md @@ -1,8 +1,9 @@ -# Project memory - -- RPN calculator JS project. -- Read `.memory/state.md` for current state. -- Keep names and commands in English. -- Update memory files based on events: engine, demo, API, commands, exports, docs, or tasks. -- Core arithmetic now includes `root` for y-th roots, and `samples/calc-02/` uses it for `y√x`. -- `samples/calc-02/` in portrait mode remains the active responsive demo. +# State +- 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 +- 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`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant` +- Config: `maxSize`, `base`, `angleMode`, `enabledCommands` +- 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 +- 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` diff --git a/.memory/state.md b/.memory/state.md index d0f5098..ab8a5cf 100644 --- a/.memory/state.md +++ b/.memory/state.md @@ -2,8 +2,8 @@ - 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 - 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` -- 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 - Exports: browser `window.RpnCalculator`, CommonJS `module.exports` diff --git a/samples/calc-02/index.js b/samples/calc-02/index.js index 0ed5d41..f3df25a 100644 --- a/samples/calc-02/index.js +++ b/samples/calc-02/index.js @@ -383,6 +383,7 @@ modeButton.addEventListener('click', (event) => { window.addEventListener('resize', () => { closeModeMenu(); closeConstMenu(); + render(); }); window.addEventListener('scroll', () => { closeModeMenu(); @@ -422,13 +423,14 @@ hiddenInput.addEventListener('paste', (event) => { upButton.addEventListener('click', () => {}); -const constants = [ - { label: 'π', value: Math.PI }, - { label: 'e', value: Math.E }, - { label: 'φ', value: (1 + Math.sqrt(5)) / 2 }, - { label: 'g', value: 9.80665 }, - { label: 'c', value: 299792458 }, -]; +const constantLabels = { + pi: 'π', + e: 'e', + phi: 'φ', + g: 'g', + c: 'c', +}; +const constantOrder = ['pi', 'e', 'phi', 'g', 'c']; let constMenuEl = null; @@ -449,14 +451,19 @@ function toggleConstMenu() { constMenuEl = document.createElement('div'); constMenuEl.className = 'mode-menu'; 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'); button.type = 'button'; 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', () => { pushEditingValueIfNeeded(); - calc.push(constant.value); + calc.push(availableConstants[name]); render(); clearStatus(); closeConstMenu(); diff --git a/src/rpn-calculator.js b/src/rpn-calculator.js index 9311f0d..1572948 100644 --- a/src/rpn-calculator.js +++ b/src/rpn-calculator.js @@ -2,6 +2,9 @@ class RpnCalculator { static CONSTANTS = { pi: Math.PI, e: Math.E, + phi: (1 + Math.sqrt(5)) / 2, + g: 9.80665, + c: 299792458, }; static OPERATIONS = { @@ -212,6 +215,15 @@ class RpnCalculator { 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) { if (this.angleMode === 'grad') { return (value * Math.PI) / 200; @@ -257,6 +269,45 @@ class RpnCalculator { 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) { if (this.stack.length >= this.maxSize) { throw new Error('Stack overflow'); @@ -408,6 +459,10 @@ class RpnCalculator { getConstants() { return { ...this.constants }; } + + listConstants() { + return this.getConstants(); + } } if (typeof window !== 'undefined') {