One Plus Two Plus Three Is Twenty-Four — JavaScript Bug Hunt

Modelled on the iOS 11 Calculator bug (2017): typing 1 + 2 + 3 = quickly gave 24.

  • Language: JavaScript
  • Layer: Frontend
  • Difficulty: Medium
  • Concepts: State, Rendering
  • Modelled on: Apple iOS 11 · 2017
  • Visible tests: slow typing adds up; fast typing adds up too
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the iOS 11 Calculator bug (2017): typing 1 + 2 + 3 = quickly gave 24. The operator keys played a highlight animation after each tap, and a tap on an operator whose animation was still running was ignored — so the second + vanished, and the input became 1 + 23. Apple fixed it in iOS 11.2.

calculator.js reproduces the keypad. Every keypress carries its own timestamp, so the tests replay fast and slow typing deterministically.

Fix press so every keypress is applied, whatever the animation is doing.

Bug report

BUG-CALC24 · Priority: High · Reported by: a lot of people

createCalculator() returns { press(key, nowMs), display() }. Keys are "0"-"9", "+", "-" and "=".

  • digits build the current entry; display() shows the entry
  • an operator applies the pending operation to the running total (the first operator just takes the entry as the total), becomes the pending operation, and display() shows the running total
  • "=" applies the pending operation, shows the result and clears the state so the next digit starts a new calculation
  • EVERY press is applied in order, however soon after the previous one it arrives. The operator animation (ui.OPERATOR_ANIMATION_MS) is cosmetic.

Observed: 1 + 2 + 3 = typed quickly shows 24; typed slowly it shows 6.

Logs

[keypad] t=0 "1"  t=60 "+"  t=120 "2"  t=180 "+" (animating, ignored)  t=240 "3"  t=300 "="
[display] 24

The code as shipped

src/calc/calculator.js (editable)

var ui = require("./ui");

exports.createCalculator = function () {
  var total = null;
  var entry = "";
  var pending = null;
  var shown = "0";
  var animatingUntil = {};

  function settle() {
    var value = entry === "" ? 0 : Number(entry);
    if (total === null) total = value;
    else if (pending === "+") total = total + value;
    else if (pending === "-") total = total - value;
    entry = "";
  }

  return {
    press: function (key, now) {
      if (ui.isOperator(key)) {
        if (now < (animatingUntil[key] || 0)) return;
        animatingUntil[key] = now + ui.OPERATOR_ANIMATION_MS;
        settle();
        pending = key;
        shown = String(total);
      } else if (key === "=") {
        settle();
        shown = String(total);
        total = null;
        pending = null;
      } else {
        entry += key;
        shown = entry;
      }
    },
    display: function () { return shown; }
  };
};

Read-only context: src/calc/ui.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.