Home Module 15 Calculator

Project Overview

Build a calculator that handles the full operation cycle: number entry, operator selection, and result calculation. This project practises state management, event delegation, keyboard events, and edge-case logic.

What you will build

  • Numeric display showing current input and expression
  • Buttons: 0–9, ., +, -, ×, ÷, =, C, ±, %
  • Keyboard support (number keys, operators, Enter, Backspace, Escape)
  • Calculation history (last 5 operations)
  • Edge case handling (divide by zero, leading zeros, multiple decimals)

Concepts used

  • State object to track operands and operator
  • Event delegation on the button grid
  • Keyboard event handling on document
  • Number parsing and floating-point rounding

Step-by-Step Build

Step 1 — State model

// The calculator's entire logic state
const state = {
  current:   '0',      // what's displayed
  previous:  '',       // first operand
  operator:  null,     // +, -, *, /
  shouldReset: false   // true after = — next digit starts fresh
};

Step 2 — Digit input

function inputDigit(digit) {
  if (state.shouldReset) {
    state.current   = digit;
    state.shouldReset = false;
    return;
  }
  if (state.current === '0' && digit !== '.') {
    state.current = digit; // replace leading zero
  } else if (digit === '.' && state.current.includes('.')) {
    return; // prevent multiple decimals
  } else {
    state.current += digit;
  }
}

Step 3 — Operator selection

function chooseOperator(op) {
  if (state.operator && !state.shouldReset) {
    calculate(); // chain: 5 + 3 × → compute 5+3 first
  }
  state.previous = state.current;
  state.operator = op;
  state.shouldReset = true;
}

Step 4 — Calculate

function calculate() {
  if (!state.operator || state.shouldReset) return;
  const a = parseFloat(state.previous);
  const b = parseFloat(state.current);
  let result;

  switch (state.operator) {
    case '+': result = a + b; break;
    case '-': result = a - b; break;
    case '*': result = a * b; break;
    case '/':
      if (b === 0) { state.current = 'Error'; state.operator = null; return; }
      result = a / b;
      break;
  }

  // Round floating-point noise (e.g. 0.1 + 0.2 = 0.30000000000000004)
  state.current  = String(parseFloat(result.toFixed(10)));
  state.operator = null;
  state.shouldReset = true;
}

Complete Working Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Calculator</title>
  <style>
    * { margin:0; padding:0; box-sizing:border-box; }
    body { min-height:100vh; display:flex; align-items:center; justify-content:center;
           background:#1a1a2e; font-family:sans-serif; }
    .calc { background:#16213e; border-radius:1rem; padding:1.5rem; width:320px;
            box-shadow:0 10px 40px #0008; }
    .display { background:#0f3460; border-radius:.5rem; padding:1rem; margin-bottom:1rem;
               text-align:right; min-height:80px; }
    .expr { font-size:.8rem; color:#888; min-height:1.2em; }
    .current { font-size:2.5rem; color:#fff; word-break:break-all; }
    .buttons { display:grid; grid-template-columns:repeat(4,1fr); gap:.6rem; }
    button {
      padding:1rem; border:none; border-radius:.5rem; font-size:1.1rem;
      cursor:pointer; transition:filter .1s;
    }
    button:hover { filter:brightness(1.2); }
    button:active { filter:brightness(.9); }
    .btn-num    { background:#334155; color:#fff; }
    .btn-op     { background:#e26d5c; color:#fff; }
    .btn-fn     { background:#1e3a5f; color:#7dd3fc; }
    .btn-eq     { background:#3b82f6; color:#fff; grid-column:span 2; }
    .btn-clear  { background:#475569; color:#fff; }
    .history    { margin-top:1rem; color:#888; font-size:.78rem; }
    .history li { padding:.2rem 0; border-bottom:1px solid #ffffff11; }
  </style>
</head>
<body>
  <div class="calc">
    <div class="display">
      <div class="expr" id="expr"></div>
      <div class="current" id="display">0</div>
    </div>
    <div class="buttons">
      <button class="btn-fn"  data-action="clear">C</button>
      <button class="btn-fn"  data-action="sign">±</button>
      <button class="btn-fn"  data-action="percent">%</button>
      <button class="btn-op"  data-op="/">÷</button>
      <button class="btn-num" data-digit="7">7</button>
      <button class="btn-num" data-digit="8">8</button>
      <button class="btn-num" data-digit="9">9</button>
      <button class="btn-op"  data-op="*">×</button>
      <button class="btn-num" data-digit="4">4</button>
      <button class="btn-num" data-digit="5">5</button>
      <button class="btn-num" data-digit="6">6</button>
      <button class="btn-op"  data-op="-">−</button>
      <button class="btn-num" data-digit="1">1</button>
      <button class="btn-num" data-digit="2">2</button>
      <button class="btn-num" data-digit="3">3</button>
      <button class="btn-op"  data-op="+">+</button>
      <button class="btn-num" data-digit="0" style="grid-column:span 2">0</button>
      <button class="btn-num" data-digit=".">.</button>
      <button class="btn-eq"  data-action="equals">=</button>
    </div>
    <ul class="history" id="history"></ul>
  </div>

  <script>
    const displayEl = document.querySelector('#display');
    const exprEl    = document.querySelector('#expr');
    const histEl    = document.querySelector('#history');
    const buttons   = document.querySelector('.buttons');

    const state = { current: '0', previous: '', operator: null, shouldReset: false };
    const history = [];

    function updateDisplay() {
      displayEl.textContent = state.current;
      exprEl.textContent = state.previous && state.operator
        ? `${state.previous} ${state.operator === '*' ? '×' : state.operator === '/' ? '÷' : state.operator}`
        : '';
    }

    function inputDigit(d) {
      if (state.shouldReset) { state.current = d; state.shouldReset = false; return; }
      if (state.current === '0' && d !== '.') { state.current = d; return; }
      if (d === '.' && state.current.includes('.')) return;
      state.current += d;
    }

    function chooseOp(op) {
      if (state.operator && !state.shouldReset) calculate(false);
      state.previous    = state.current;
      state.operator    = op;
      state.shouldReset = true;
    }

    function calculate(addHistory = true) {
      if (!state.operator || state.shouldReset) return;
      const a = parseFloat(state.previous);
      const b = parseFloat(state.current);
      let result;
      const expr = `${a} ${state.operator} ${b}`;
      switch (state.operator) {
        case '+': result = a + b; break;
        case '-': result = a - b; break;
        case '*': result = a * b; break;
        case '/':
          if (b === 0) { state.current = 'Error'; state.operator = null; updateDisplay(); return; }
          result = a / b; break;
      }
      state.current = String(parseFloat(result.toFixed(10)));
      if (addHistory) {
        history.unshift(`${expr} = ${state.current}`);
        if (history.length > 5) history.pop();
        histEl.innerHTML = history.map(h => `<li>${h}</li>`).join('');
      }
      state.operator    = null;
      state.shouldReset = true;
    }

    buttons.addEventListener('click', e => {
      const btn = e.target.closest('button');
      if (!btn) return;
      const { digit, op, action } = btn.dataset;
      if (digit !== undefined) { inputDigit(digit); }
      else if (op)             { chooseOp(op); }
      else if (action === 'equals')  { calculate(); }
      else if (action === 'clear')   { Object.assign(state, { current:'0', previous:'', operator:null, shouldReset:false }); }
      else if (action === 'sign')    { state.current = String(parseFloat(state.current) * -1); }
      else if (action === 'percent') { state.current = String(parseFloat(state.current) / 100); }
      updateDisplay();
    });

    document.addEventListener('keydown', e => {
      if ('0123456789.'.includes(e.key)) { inputDigit(e.key); }
      else if (['+','-','*','/'].includes(e.key)) { chooseOp(e.key); }
      else if (e.key === 'Enter' || e.key === '=') { calculate(); }
      else if (e.key === 'Escape') { Object.assign(state, { current:'0', previous:'', operator:null, shouldReset:false }); }
      else if (e.key === 'Backspace') {
        if (!state.shouldReset && state.current.length > 1) {
          state.current = state.current.slice(0, -1);
        } else {
          state.current = '0';
        }
      }
      else return;
      updateDisplay();
    });

    updateDisplay();
  </script>
</body>
</html>

Code Explained

State machine approach

The calculator uses a state object with four properties. shouldReset is the key insight — after pressing = or an operator, the next digit should start a fresh number rather than appending to the current one.

Operator chaining

If the user types 5 + 3 ×, the second operator (×) triggers calculate(false) first (computing 5+3=8), then stores 8 as previous with the new operator. This matches standard calculator behaviour.

toFixed(10) + parseFloat

Floating-point arithmetic in JavaScript: 0.1 + 0.2 === 0.30000000000000004. Rounding to 10 decimal places with toFixed, then parsing back to a number with parseFloat removes the trailing noise while preserving precision for normal use.

Challenges

  1. Add a backspace button that removes the last digit.
  2. Add memory functions: M+, M-, MR, MC.
  3. Support keyboard input for the Backspace key.
  4. Add a scientific mode with square root, power, and logarithm buttons.
  5. Style the active operator button differently (highlight which operator is selected).