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.
Letās add a test case before we continue. It should 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!
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.
Letās write a new test case to ensure Number->Operator->Operator works.
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!
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.
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
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).
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.
We used data-first-value as firstValue for the second calculation.
We used the displayed value (which is the result) as the secondValue for the second calculation.
Hereās the correct version:
We should assign the result of the calculation to firstValue.
We should reuse 9 as secondValue.
Itās easy to assign the result to firstValue. We can assign it after the calculation.
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.
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! š± š± š±!
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:
Test for Number->Operator->Equal->Equal failed
Test for Number->Operator->Number->Equal->Equal passed
Why?
Once again, we can log firstValue, operator, secondValue, and newResult to figure out the reason.
Letās see what our calculator does when we make followup calculations right now.
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:
This says:
We should replace firstValue with the calculated result.
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.
We should be able to perform follow-up calculations with operator keys now.
Unfortunately, this new code breaks the Calculation + Operator test.
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
Letās punch in this sequence and see whatās up.
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.
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
Write down all the cases I mentioned on a piece of paper.
Build the calculator again from scratch
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!