TheOdinProject – Calculator: updating the display and mathematical operations

In the previous article, I wrote about the first part of the application, the part where we get numbers and operators.


Now that we have them in the application flow, it’s time to describe the next steps: calculations and updating the display

JS code:
let firstNumber = '';
let secondNumber = '';
let operator = '';
let intermediateResult = ''; 
let isEnteringSecondNumber = false; 
let errorMessage = null;
let display = document.getElementById('uiDisplay');

function updateDisplay() {
    let displayText = '';

    if (errorMessage) {
        isEnteringSecondNumber = false;
        displayText = errorMessage;
        if (operator) {
            return;
        }
    } else if (intermediateResult) {
        let roundedResult = parseFloat(intermediateResult).toFixed(2);
        if (intermediateResult % 1 !== 0) {
            displayText += roundedResult;
        } else {
            displayText += intermediateResult;
        }
    } else {
        displayText += firstNumber || '0';
    }

    if (operator !== "=") {
        displayText += `${operator}`;
    }

    if (isEnteringSecondNumber) {
        displayText += `${secondNumber}`;
    }

    if (displayText.length >= 18){
        display.classList.add("big-number");
    } else {
        display.classList.remove("big-number");
    }

    display.innerHTML = displayText;
}

function operate() {
    if (operator && secondNumber) {
        if (intermediateResult) {
            firstNumber = intermediateResult;
            intermediateResult = '';
        }
        intermediateResult = calculate(operator, firstNumber, secondNumber);
        secondNumber = '';
        operator = '';
        updateDisplay();
    }
}

function calculate(operator, a, b) {
    let num1 = +a;
    let num2 = +b;
    if (operator === "/") {
        return divide(num1, num2);
    } else if (operator === "*") {
        return multiply(num1, num2);
    } else if (operator === "-") {
        return subtract(num1, num2);
    } else {
        return add(num1, num2);
    }
}

function divide(a, b) {
    if (b === 0) {
        errorMessage = "Cannot divide by 0"; 
        return null;
    } else {
        errorMessage = null; 
        return a / b;
    }
}

function multiply(a, b) {
    return a * b;
}

function subtract(a, b) {
    return a - b;
}

function add(a, b) {
    return a + b;
}

function handleNumberInput(number) {
    if (operator && isEnteringSecondNumber) {
        if (number === '.' && secondNumber.includes('.') || secondNumber.length >= 16) {
            return;
        } else {
            secondNumber += number;
        }
    } else {
        if (number === '.' && firstNumber.includes('.') || firstNumber.length >= 16) {
            return;
        } else {
            firstNumber += number;
        }
    }
    updateDisplay();
}

function handleOperatorInput(selectedOperator) {
    if (firstNumber && secondNumber && operator) {
        operate();
    }

    if (operator === "=" && firstNumber === '' && secondNumber === '') {
        return;
    }
    operator = selectedOperator;
    isEnteringSecondNumber = true;
    updateDisplay();
}

function handleKeyboardInput(event) {
    let pressedKey = event.key;

    if (pressedKey === "/") {
        event.preventDefault();
    }

    if (/[+\-*/]/.test(pressedKey)) {
        handleOperatorInput(pressedKey);
    } else if (pressedKey === "=" || pressedKey === "Enter") {
        operate();
    } else if (pressedKey === "Backspace") {
        deleteFromNumber();
    } else if (pressedKey === "Escape") {
        clear();
    } else if (/^[0-9.]$/.test(pressedKey)) {
        handleNumberInput(pressedKey);
    } else {
        return;
    }}

function deleteFromNumber() {
    if (operator && isEnteringSecondNumber) {
        secondNumber = secondNumber.slice(0,-1);
    } else {
        firstNumber = firstNumber.slice(0,-1);
    };
    errorMessage = null; 
    updateDisplay();
}

function clear() {
    firstNumber = '';
    secondNumber = '';
    operator = '';
    intermediateResult = ''; 
    isEnteringSecondNumber = false; 
    errorMessage = null; 
    updateDisplay();
}

document.querySelectorAll('.number').forEach(function (button) {
    button.addEventListener('click', function () {
        handleNumberInput(button.innerText);
    });
});

document.querySelectorAll('.operator').forEach(function (button) {
    button.addEventListener('click', function () {
        handleOperatorInput(button.innerText);
    });
});

document.getElementById('equals').addEventListener('click', function () {
    operate();
});

document.getElementById('delete').addEventListener('click', function () {
    deleteFromNumber();
});

document.getElementById('clear').addEventListener('click', function () {
    clear();
});

window.addEventListener('keydown', function (event) {
    handleKeyboardInput(event);
});

updateDisplay();

1. Lines 46-57:
the operate() function is called from the handleOperatorInput() function that I described in the previous article. From there we get the first number, second number and the operator required for the mathematical operation.

The function test if operator and secondNumber are true (the user has inputed both numbers and an operator required for a calculation); if yes: tests if the variable intermediateResult is true.
What is intermediateResult? In short, my solution to the request:

Your calculator should not evaluate more than a single pair of numbers at a time. Example: you press a number button (12), followed by an operator button (+), a second number button (7), and finally a second operator button (-). Your calculator should then do the following: first, evaluate the first pair of numbers (12 + 7), second, display the result of that calculation (19), and finally, use that result (19) as the first number in your new calculation, along with the next operator (-).

After the user makes the first calculation, it is stored in intermediateResult.
In line 48 we check if an operation has been performed, thus firstNumber variable is assigned the value of intermediateResult and the last one is emptied for future use, signaling the app that a another mathematical operation is posible.

If intermediateResult is empty, we call the calculate() function with the parameters (operator, firstNumber, secondNumber).

After we have the result stored in intermediateResult, we empty the secondNumber and operator variables for future use, then we call updateDisplay() to display the value of the calculation, but more on that soon…

2. Lines 59-93.
I practically reached the section that does the heavy lifting, that puts the calculation in the calculator.

The calculate() function takes as parameters (the operator, a[firstNumber], b[secondNumber]).

Then we store them in num1 and num2 variable, where we make sure that they are numbers by adding + as prefix to the assigned parameter.

For each operation, we have an if case that returns the result of the called function, depending on the operators. For example, on line 54, if the operator is "*" (multiplication), multiply(num1, num2) is called and the result if said math operation is returned.

For each mathematical operation, there is a distinct divide, multiply, subtract and add function, each of which takes numbers as parameters and returns the result of the operation. I don’t think they need extraordinary explanations, with the exception of:

Line 73 divide() function.

One of the requirements of the application is the following:

Display a snarky error message if the user tries to divide by 0… and don’t let it crash your computer!

So we check if the second number (b) is 0 and if it is we will set the errorMessage with “Cannot divide by 0” and return a null to exit the normal flow of the application to prevent a browser crash. The rest of the operations will take place in the updateDisplay() function depending on the returned null.

If b is not 0, errorMessage is set as null, to delete a possible previous error and the division result is returned.

3. Lines 9-44.
updateDisplay(), the function that we have seen called the most so far, takes care of updating the display of the application according to the data it receives.

On line 10, we declare the displayText variable that will be used locally throughout the entire function.

Within the function we have a series of if”s that work independently of each other:

The first one contained between lines 12 and 27 checks if there is an error message, return to step 1 of the app flow, by setting isEnteringSecondNumber = false and display the error message. By doing so the app wil expect the input of the second number.
Also, if an operator is detected, the application flow is interrupted with a return. (line 16)

If everything is ok, we go to else if and check if we have set intermediateResult:

  • Here we initialize roundedResult, for which we use the value of intermediateResult in a parseFloat().toFixed(2), so we make sure that we won’t have more than 2 decimals in case decimals were used.
  • In the first if we check whether the remainder (%) of the division of intermediateResult by 1 is not equal to 0: In the case the result is not 0, the number is not an integer displayText takes the value of roundedResult, otherwise the number is an integer and we assign the value of intermediateResult.

 

  • Line 25. The last else in the main displayText if check, looks at the value of firstNumber. If the variable contains a value, the value is displayed, if not, ‘0’ is displayedL
  • Lines 29-31: we check that if the operator is not “=”. If it`s not we add it to the displayText variable in order to display it in the UI. The "=" operator is not displayed in the UI.
  • On lines 33-45: we check if the user enters the second number from the operation, thus setting displayText with a template literal${secondNumber}.

The last check in the function checks if the size of the number to be displayed is equal to or greater than 18 characters:

  • If yes, add the big-number class on the display in the UI,
  • if it drops below 18 characters, remove the class.

The class reduces the font size for the display on the UI from 2.5rem to 2rem.

On the last line, we set as innerHTML of the display variable the content of the displayText variable, obtained from the checks above.

 


If you want to see an index with all the articles for this application, go to Articles by projects.

Github page / Live version.

Leave a Reply

Your email address will not be published. Required fields are marked *