šŸ› ļø Calculator: Difficult Edge Cases

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

šŸ› ļø Calculator: Difficult Edge Cases

The difficult edge cases involve follow-up calculations. Donā€™t get what I mean? Donā€™t worry. Letā€™s dive in and youā€™ll see.

Calculating with operators

Try whipping out a calculator. Press keys in these sequence: Number -> Operator -> Number -> Operator. What happens? The calculator should have made a calculation.

For example:

  • Press 9 - 5 -. Calculator should show 4.

But why?

If the user press 9 - 5 -, we can assume they want to press another number to continue their calculation. We want to show them an intermediate result so they can keep track of their results.

Right now, our calculator doesnā€™t perform a calculation. It seems doesnā€™t do anything when you press the second operator.

Punch 9 - 5 - into the calculator. Calculator shows 5.

Letā€™s add a test case before we continue. It should fail.

const tests = [
  // ...
  {
    message: 'Operator calculation',
    keys: ['9', 'minus', '5', 'minus'],
    result: '4'
  }
]
Assertion fail.

The easiest way to make a calculation is to copy-paste code from the equal section into the operator section.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    button.classList.add('is-pressed')

    const firstValue = parseFloat(calculator.dataset.firstValue)
    const operator = calculator.dataset.operator
    const secondValue = parseFloat(result)

    // Makes a calculation
    if (firstValue && operator) {
      let newResult
      if (operator === 'plus') newResult = firstValue + secondValue
      if (operator === 'minus') newResult = firstValue - secondValue
      if (operator === 'times') newResult = firstValue * secondValue
      if (operator === 'divide') newResult = firstValue / secondValue

      display.textContent = newResult
    }

    calculator.dataset.firstValue = result
    calculator.dataset.operator = button.dataset.key
  }
  // ...
})

The test passes!

Clicks 9 - 5 -. Calculator shows 4.

Unfortunately, this new code screws up a Number -> Operator -> Operator combination (which we did not test before).

Number -> Operator -> Operator

If the user presses an operator after another operator, we assume they pressed the wrong operator the first time round. They simply wanted to switch operators, so we should not perform calculations.

Right now, our calculator performs a calculation.

Clicks 9 * -. Caluclator shows 81.

Letā€™s write a new test case to ensure Number -> Operator -> Operator works.

const tests = [
  // ...
  {
    message: 'Number Operator Operator',
    keys: ['9', 'times', 'divide'],
    result: '9'
  }
]
Assertion failed.

We know how to fix this. If the previous button is an operator, we want to skip the calculation entirely.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    // ...

    if (previousButtonType === 'operator') {
      // Do nothing
    } else {
      const firstValue = parseFloat(calculator.dataset.firstValue)
      const operator = calculator.dataset.operator
      const secondValue = parseFloat(result)

      // Makes a calculation
      if (firstValue && operator) {
        let newResult
        if (operator === 'plus') newResult = firstValue + secondValue
        if (operator === 'minus') newResult = firstValue - secondValue
        if (operator === 'times') newResult = firstValue * secondValue
        if (operator === 'divide') newResult = firstValue / secondValue

        display.textContent = newResult
      }
    }

    // ...
  }
  // ...
})

This works. All our tests passed!

Punch 9 times minus into the calculator. Calculator shows 9.

But itā€™s weird to have a do nothing comment in an if statement. We can reverse the if to make the code cleaner.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    // ...

    // Reverses the if condition
    if (previousButtonType !== 'operator') {
      const firstValue = parseFloat(calculator.dataset.firstValue)
      const operator = calculator.dataset.operator
      const secondValue = parseFloat(result)

      // Makes a calculation
      if (firstValue && operator) {
        let newResult
        if (operator === 'plus') newResult = firstValue + secondValue
        if (operator === 'minus') newResult = firstValue - secondValue
        if (operator === 'times') newResult = firstValue * secondValue
        if (operator === 'divide') newResult = firstValue / secondValue

        display.textContent = newResult
      }
    }

    // ...
  }
  // ...
})

Since we will only calculate if previousButtonType !== 'operator', and if thereā€™s a firstValue and operator value, we can combine the two if statements into one. This cleans up the code a bit.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    // ...
    const firstValue = parseFloat(calculator.dataset.firstValue)
    const operator = calculator.dataset.operator
    const secondValue = parseFloat(result)

    // Combines two if statements into one
    if (
      previousButtonType !== 'operator' &&
      firstValue &&
      operator
    ) {
      let newResult
      if (operator === 'plus') newResult = firstValue + secondValue
      if (operator === 'minus') newResult = firstValue - secondValue
      if (operator === 'times') newResult = firstValue * secondValue
      if (operator === 'divide') newResult = firstValue / secondValue

      display.textContent = newResult
    }

    // ...
  }
  // ...
})

Follow up calculations

Once users press an operator key, they can perform follow up calculations with BOTH the operator and equal keys. This gets complicated.

Here are the possible combinations:

  1. Number -> Operator -> Equal -> Equal
  2. Number -> Operator -> Number -> Equal -> Equal
  3. Number -> Operator -> Number -> Operator -> Number -> Operator ->

Follow up calculations with equal

These two follow up calculations uses the equal key:

  1. Number -> Operator -> Equal -> Equal
  2. Number -> Operator -> Number -> Equal -> Equal

Example:

  1. Tim presses 9 - =. We take it as 9 - 9. Calculator shows 0. Tim presses = again. We do 0 - 9, so calculator shows - 9.
  2. Tim presses 8 - 5 =. We show 3. Tim presses = again. We do 3 - 5, so calculator shows -2.

Letā€™s add test cases before we continue.

const tests = [
  // ...
  {
    message: 'Number Operator Equal Equal',
    keys: ['9', 'minus', 'equal', 'equal'],
    result: '-9'
  }, {
    message: 'Number Operator Number Equal Equal',
    keys: ['8', 'minus', '5', 'equal', 'equal'],
    result: '-2'
  }
]

Both tests should fail.

Assertions fail.

Weā€™ll tackle them together. This part is tricky; follow closely!

Letā€™s look at the first case. Hereā€™s what our calculator does when we press 9 - = =Ā .

Clicks 9 - =. Shows 0. Clicks = again. Shows 9.

Why does the calculator show 9, 0, then 9? We can log firstValue, operator, secondValue and newResult into the console to figure out why.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
if (buttonType === 'equal') {
    // ...
    if (firstValue && operator) {
      let newResult
      if (operator === 'plus') newResult = firstValue + secondValue
      if (operator === 'minus') newResult = firstValue - secondValue
      if (operator === 'times') newResult = firstValue * secondValue
      if (operator === 'divide') newResult = firstValue / secondValue
      display.textContent = newResult

      // console.log values.
      console.log(firstValue, operator, secondValue, '=', newResult)
    } else {
      display.textContent = parseFloat(result) * 1
    }
  }
  // ...
})

Note: You will see lots of logs when you refresh the page. These logs were created by our test cases. If they bother you, you can comment out the tests for now.

(Another way is to clear the console before clicking the calculator).

Click the calculator according to the Number -> Operator -> Equal -> Equal test case. Pay attention to the values for firstValue, operator, and secondValue

Logs firstValue, operator, secondValue, and newResult for each calculation.

Itā€™s hard to understand whatā€™s happening from logs. Letā€™s put down these values down on paper so we can search for clues. (I actually wrote them down on paper).

Putting the logs down on paper.

For the first calculation:

  • firstValue is 9 (which is correct)
  • secondValue is also 9 (also correct).

For the second calculation:

  • firstValue is 9 (which is wrong).
  • secondValue is 0 (also wrong)

Why? Hereā€™s the reason.

  1. We used data-first-value as firstValue for the second calculation.
  2. We used the displayed value (which is the result) as the secondValue for the second calculation.
Same image as above, but with arrows to link where the logic went wrong.

Hereā€™s the correct version:

  1. We should assign the result of the calculation to firstValue.
  2. We should reuse 9 as secondValue.
The correct flow of calculations.

Itā€™s easy to assign the result to firstValue. We can assign it after the calculation.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'equal') {
    const firstValue = parseFloat(calculator.dataset.firstValue)
    const operator = calculator.dataset.operator
    const secondValue = parseFloat(result)

    if (firstValue && operator) {
      let newResult
      if (operator === 'plus') newResult = firstValue + secondValue
      if (operator === 'minus') newResult = firstValue - secondValue
      if (operator === 'times') newResult = firstValue * secondValue
      if (operator === 'divide') newResult = firstValue / secondValue
      display.textContent = newResult

      // Assigns results to `firstValue`
      calculator.dataset.firstValue = newResult
    } else {
      display.textContent = parseFloat(result) * 1
    }
  }
  // ...
})

Setting 9 to secondValue is tricky because the number 9 disappears from the display. We need to save 9 as a custom attribute. Letā€™s call this custom attribute data-modifier-value.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'equal') {
    const firstValue = parseFloat(calculator.dataset.firstValue)
    const operator = calculator.dataset.operator
    const secondValue = parseFloat(result)

    if (firstValue && operator) {
      let newResult
      if (operator === 'plus') newResult = firstValue + secondValue
      if (operator === 'minus') newResult = firstValue - secondValue
      if (operator === 'times') newResult = firstValue * secondValue
      if (operator === 'divide') newResult = firstValue / secondValue

      display.textContent = newResult
      calculator.dataset.firstValue = newResult

      // Stores secondValue as modifier for followup calculations.
      calculator.dataset.modifierValue = secondValue
    } else {
      display.textContent = parseFloat(result) * 1
    }
  }
  // ...
})

Before the next calculation, we need to check if modifierValue is present. If it is present, we use modifierValue as secondValue. Otherwise, we use the displayed result as the secondValue.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'equal') {
    const firstValue = parseFloat(calculator.dataset.firstValue)
    const operator = calculator.dataset.operator
    // Finds modifier value
    // Use modifier value as secondValue (if possible)
    const modifierValue = parseFloat(calculator.dataset.modifierValue)
    const secondValue = modifierValue || parseFloat(result)

    if (firstValue && operator) {
      let newResult
      if (operator === 'plus') newResult = firstValue + secondValue
      if (operator === 'minus') newResult = firstValue - secondValue
      if (operator === 'times') newResult = firstValue * secondValue
      if (operator === 'divide') newResult = firstValue / secondValue

      display.textContent = newResult
      calculator.dataset.firstValue = newResult
      calculator.dataset.modifierValue = secondValue
    } else {
      display.textContent = parseFloat(result) * 1
    }
  }
  // ...
})

When we make this change, we break A TON of tests! šŸ˜± šŸ˜± šŸ˜±!

Many tests fail.

Donā€™t panic!

Itā€™s normal to break tests when you add features. In this case, we started using modifierValue in our calculations, but we did not reset modifierValue when we reset the calculator. This is why our tests broke.

Hereā€™s a simple fix:

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'clear') {
    if (button.textContent === 'AC') {
      delete calculator.dataset.firstValue
      delete calculator.dataset.operator
      // Clearing the modifier value
      delete calculator.dataset.modifierValue
    }

    display.textContent = '0'
    button.textContent = 'AC'
  }
  // ...
})

Weā€™ll also updated resetCalculator to ensure modifierValue gets reset.

The previous tests should now pass. Youā€™ll also notice this:

  1. Test for Number -> Operator -> Equal -> Equal failed
  2. Test for Number -> Operator -> Number -> Equal -> Equal passed
Only Number Operator Equal Equal follow up calculations fail.

Why?

Once again, we can log firstValue, operator, secondValue, and newResult to figure out the reason.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'equal') {
    // ...

    if (firstValue && operator) {
      // ...
      // Console.log values.
      console.log(firstValue, operator, secondValue, '=', newResult)
    } else {
      display.textContent = parseFloat(result) * 1
    }
  }
  // ...
})
Debugs the above picture.

We can see the calculator made the first calculation. But it doesnā€™t make the second calculation no matter how many times we press equal.

Why?

Because firstValue became 0, which is falsey. So this condition became false šŸ˜°.

if (firstValue && operator) {
  // ...
}

This is a good lesson for us: donā€™t use truthiness to check numbers. If we want to check if a number exists, we can use typeof.

// Examples of how typeof works
console.log(typeof 42) // number
console.log(typeof 42 === 'number') // true

Usage:

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'equal') {
    // ...

    if (typeof firstValue === 'number' && operator) {
      // Make calculation
    } else {
      // ...
    }
  }
  // ...
})

All tests should pass. The calculator can perform followup calculations with equal now.

  1. Number -> Operator -> Equal -> Equal
  2. Number -> Operator -> Number -> Equal -> Equal
Number -> Operator -> Equal -> Equal
Number -> Operator -> Number -> Equal -> Equal

Follow up calculations with operators

Users can perform a calculation with operator keys if they use the following sequence:

  • Number -> Operator -> Number -> Operator

We should also let them perform additional calculations if they press more of this sequence:

  • Number -> Operator -> Number -> Operator -> Number -> Operator -> And so on.

For example, letā€™s say Tim presses keys in this order: 1 + 2 + 3 + 4 + 5 +.

This should happen:

  1. 1 + 2 +. Calculator shows 3.
  2. 3 +. Calculator shows 6.
  3. 4 +. Calculator shows 10.
  4. 5 +. Calculator shows 15.

Letā€™s create a test case before we continue.

const test = [
  // ...
  {
    message: 'Operator follow-up calculation',
    keys: ['1', 'plus', '2', 'plus', '3', 'plus', '4', 'plus', '5', 'plus'],
    result: '15'
  }
]

This should fail (as expected).

Assertion failed.

Letā€™s see what our calculator does when we make followup calculations right now.

Punched 1 + 2 + 3 + 4 + 5 + into the calculator.

We know the test failed. Itā€™s not a surprise our calculator made the wrong calculations.

We need to follow this format to correct the calculations:

How to create follow-up operator calculations

This says:

  1. We should replace firstValue with the calculated result.
  2. We should get secondValue from the display.

We already get secondValue from the display, so thereā€™s nothing to do for point 2. For point 1, hereā€™s how you can replace firstValue with the calculated result.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    // ...

    if (
      previousButtonType !== 'operator' &&
      firstValue &&
      operator
    ) {
      // ...

      display.textContent = newResult

      // If there's a calculation, we change firstValue
      calculator.dataset.firstValue = newResult
    } else {
      // Otherwise, we set displayed result to firstValue
      calculator.dataset.firstValue = result
    }

    calculator.dataset.operator = button.dataset.key
  }
  // ...
})

Like the equal section, we need to check whether firstValue is a Number. We should use typeof for this check.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    // ...
    if (
      previousButtonType !== 'operator' &&
      typeof firstValue === 'number' &&
      operator
    ) {
      // ...
    } else {
      // ...
    }

    calculator.dataset.operator = button.dataset.key
  }
  // ...
})

We should be able to perform follow-up calculations with operator keys now.

Clicks 1 + 2 + 3 + 4 + 5. Shows 15 at the end.

Unfortunately, this new code breaks the Calculation + Operator test.

Assertion failed.

Itā€™s not fun to see our tests break. But itā€™s even more un-fun to get an error yourself much later without even knowing what went wrong. Letā€™s fix this.

Fixing the Calculation + Operator test

So, whatā€™s wrong? Letā€™s take a quick look at our tests again. Hereā€™s the Calculation + operator test

const test = [
  // ...
  {
    message: 'Calculation + Operator',
    keys: ['1', 'plus', '1', 'equal', 'plus', '1', 'equal'],
    result: '3'
  },
]

Letā€™s punch in this sequence and see whatā€™s up.

Punching in 1 + 1 = + 1 =.

Oh, looks like the calculator made a calculation when you click Equal -> Operator. This should not happen. We can fix this easily by checking if the previous button is equal.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    // ...

    if (
      previousButtonType !== 'operator' &&
      // Checks if previous button is equal key
      previousButtonType !== 'equal' &&
      typeof firstValue === 'number' &&
      operator
    ) {
      // ...
    } else {
      // ...
    }
    calculator.dataset.operator = button.dataset.key
  }
  // ...
})

BAM! The test should now be fixed šŸ˜ƒ.

Punch in 1 + 1 = + 1 =. Calculator shows 3.

Wrapping up

Thatā€™s it!

Building a calculator is hard! It was never easy. Pat yourself on the back for following this through. You are MUCH better than many developers out there now. Try challenging them to build a calculator and watch them sufferā€¦ šŸ˜ˆ.

(I know how bad theyā€™ll suffer because I made the calculator without tests TWICE. It was šŸ¤®. But with tests, this is easy-peasy. We just have to fix things one after another. ).

Homework

  1. Write down all the cases I mentioned on a piece of paper.
  2. Build the calculator again from scratch
  3. See if you can find any cases I missed :)

Take your time, clear away your bugs one by one and youā€™ll get your calculator up eventually. Happy coding!