Why a Dual-State Calculator Needs an Explicit Equals Button
A two-pane calculator looks simple: place calculator A next to calculator B and let both accept input. The difficult part is not drawing two keypads. It is deciding exactly which state a key is allowed to change, when an unfinished expression becomes a result, and what should be written to history.
Twin Calculator uses an explicit equals button for each pane. It does not continuously evaluate both panes after every keystroke. That choice gives the user a clear commit point, keeps errors inside one calculator, and makes calculation history describe completed actions rather than every temporary input.
The short version
Each calculator owns its own entry, accumulator, pending operator, and error flag. A digit edits only the focused calculator. The equals action commits only that calculator's pending operation and creates one history record. The other pane remains untouched.
Two displays are not enough
If the implementation stores only two display strings, it cannot reliably distinguish these situations:
12is being entered.12 +is waiting for a second operand.12 + 3is ready to be committed.15is the result of a completed operation.- The next digit should replace the result rather than append to it.
A compact calculator state needs more information.
function newCalc() {
return {
raw: "0",
acc: null,
op: null,
fresh: true,
err: false
};
}
const calcs = {
A: newCalc(),
B: newCalc()
};
raw preserves the entry as the user typed it, including a trailing decimal point. acc holds the left operand, op holds the pending operation, and fresh tells the next digit whether to start a new entry. The error flag prevents an invalid result from leaking into the next operation.
The panes can share pure arithmetic functions and rendering code, but they must not share mutable calculation state.
Explicit equals is a commit boundary
Consider a user comparing two prices. Calculator A contains 35000 + 12500; calculator B contains a percentage calculation that is not finished yet.
With continuous evaluation, every intermediate key can change a displayed “result.” The interface must then answer difficult questions:
- Is the visible number an input or a committed result?
- Should an incomplete expression appear in history?
- If calculator A fails, should calculator B update too?
- When the user copies a number, are they copying a stable result or a preview?
An explicit equals button answers all four. It marks the moment when a pending operation becomes a result.
function pressEquals(c, tag) {
if (c.err || c.op === null) return;
const right = rawToNum(c.raw);
const expression = `${fmtNum(c.acc)} ${OPSYM[c.op]} ${fmtNum(right)}`;
const result = applyOp(c.acc, c.op, right);
if (!Number.isFinite(result)) {
setErr(c);
return;
}
c.raw = numToRaw(result);
c.acc = null;
c.op = null;
c.fresh = true;
pushTape(tag, expression, c.raw);
}
Before equals, the state is editable intent. After equals, it is a completed calculation. That boundary is valuable even when the arithmetic itself could be evaluated earlier.
Route every key to one owner
Physical keyboard input creates an additional problem: there is only one Enter key, but there are two calculators. The UI needs an explicit focus owner.
let focus = "A";
function handleKey(tag, key) {
const calc = calcs[tag];
if (/^[0-9]$/.test(key) || key === ".") {
pressDigit(calc, key);
} else if (OPS.includes(key)) {
pressOp(calc, key);
} else if (key === "equals") {
pressEquals(calc, tag);
}
render();
save();
}
Clicking inside a pane or moving keyboard focus into it sets the owner. Enter and = are then routed to that tag only.
document.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== "=") return;
event.preventDefault();
handleKey(focus, "equals");
});
The focused pane should have a visible indicator. Hidden focus would make correct internal routing feel random to the user.
Keep errors local
Division by zero is a good test of state ownership. If calculator A evaluates 1 ÷ 0, A should enter an error state while B keeps its current entry and pending operation.
function applyOp(a, op, b) {
switch (op) {
case "add": return a + b;
case "sub": return a - b;
case "mul": return a * b;
case "div": return b === 0 ? NaN : a / b;
default: return b;
}
}
function setErr(calc) {
calc.err = true;
calc.raw = "\0err";
calc.acc = null;
calc.op = null;
calc.fresh = true;
}
The next digit can reset only the failed calculator. There is no reason to clear a valid comparison in the other pane.
This is a general UI rule: components may share behavior without sharing failure state. Reusing one engine function is good. Reusing one global accumulator for both panes is not.
History should record decisions, not typing
Twin Calculator keeps a short tape of completed results. The equals boundary makes the history rule straightforward:
function pushTape(tag, expression, raw) {
tape.unshift({
tag,
expr: expression,
raw,
ts: Date.now()
});
if (tape.length > 50) tape.length = 50;
}
Each record answers three useful questions: which calculator produced it, which expression produced it, and what raw value can be loaded back. Typing 7 without pressing equals creates no history. Completing 1 + 2 = creates exactly one row.
Continuous evaluation tends to produce either noisy history or complicated suppression rules. An explicit commit action keeps the model explainable.
Do not use JavaScript source evaluation
A calculator may be tempted to build a string such as "2+3*4" and pass it to eval(). A small state machine is safer and gives the product control over semantics.
Twin Calculator uses immediate execution: entering 2 + 3 × 4 = first commits 2 + 3 when the multiplication operator is pressed, then multiplies the intermediate result by 4, producing 20. This matches a traditional four-function calculator, not algebraic precedence.
That behavior is neither universally right nor wrong. What matters is that it is intentional, visible, and tested. If a product wants algebraic precedence, it should use a real parser and make the full expression visible rather than silently changing the engine.
Persist the state that changes the next key
A side-panel calculator can remain open while the user browses, but the panel can still be recreated. The Chrome Side Panel API provides a persistent browsing surface, not a promise that in-memory JavaScript state will live forever.
The state worth restoring includes more than the displayed numbers:
- A and B entries
- each pending accumulator and operator
- the focused calculator
- calculation history
- the current view and visual settings
The extension stores those values with chrome.storage.local. Chrome's Storage API documentation describes the API as asynchronous and notes that writes have a performance cost, so the implementation batches rapid keypresses with a short delay.
let saveTimer = 0;
function save() {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
chrome.storage.local.set({ calcs, tape, focus });
}, 250);
}
On restore, saved objects should be validated. A missing accumulator with a pending operator is an impossible state and should be normalized rather than allowed to crash the next keypress.
Test transitions, not just answers
Checking that 1 + 2 = 3 is necessary but not sufficient. Most dual-state bugs live between operations.
High-value tests include:
1 + 2 =produces3and one history row.- Entering
7without equals produces no history. - A result followed by a digit starts a new entry.
- Pressing equals without a pending operator is harmless.
- Replacing an operator, as in
5 + × 3 =, has a defined result. - A's pending multiplication survives while B completes an addition.
- A division-by-zero error does not alter B.
- Reloading restores both pending operators and the keyboard focus owner.
- Decimal entry preserves
0.and trailing zeroes while typing. - The focused pane alone responds to physical
Enter.
A useful isolation test is especially small:
calcs.A = newCalc();
calcs.B = newCalc();
handleKey("A", "5");
handleKey("A", "mul");
handleKey("B", "2");
handleKey("B", "add");
handleKey("B", "3");
handleKey("B", "equals");
// B is 5; A is still waiting to multiply 5.
This verifies ownership, not merely arithmetic.
When live evaluation does make sense
Explicit equals is not the only valid design. Live evaluation works well when the product is an expression editor, a unit converter, or a formula preview where the entire input remains visible and the result is clearly labeled as a preview.
It is less attractive when:
- multiple calculators accept input at once;
- history should contain intentional results;
- intermediate errors must not interrupt a comparison;
- users frequently copy or reuse stable results;
- the interface follows traditional calculator key sequences.
The decision should follow the interaction model, not the fact that modern JavaScript can compute quickly.
Conclusion
The equals button in a dual-state calculator is not decorative. It is a transaction boundary. It turns editable input into a stable result, creates one meaningful history record, and limits the change to one clearly identified owner.
Once A and B have independent state objects, focused input routing, local error handling, and validated persistence, the interface becomes predictable. The user can leave one calculation unfinished, complete another, compare the two, and return without the application guessing what should have been committed.
Comments
Post a Comment