Building a JavaScript Calculator

As a JavaScript project I decided to build a calculator. There are a lot of them out there, but I wanted to make it a challenge by not looking at what anyone else had done and just figuring it out for myself.

The first step was the HTML. Because the keys are in a grid I used CSS grid layout. I knew that the number buttons would need to be targeted differently than the other keys, so I gave them all the class of “number”, including the decimal point. I also needed an output area to show the entries and the results. For functions, I needed the basic arithmetic operators (+ , – , * , / , and =) and a key to clear the input field. Then I threw some CSS at it to lay it all out and make it look nice.

Using the “button” element gave me a nice bevel and shadow on the keys with no extra CSS. I used a linear gradient with a stop at 68px to produce the two-color background on the calculator.

 

Completed Calculator
The Completed Calculator UI

For the script, I began by assigning click handlers to all of my calculator keys, so I could capture their input when they were clicked. To do this, I made an array of all my buttons and looped through them assigning a click handling function that I would make later. I used the ECMA6 “for…of” statement because it is more concise.

const keys = document.getElementsByTagName("button");
for (const key of keys) {
key.onclick = handleClick;
}

Next, I needed to declare the variables I would be using. This is where I did most of my thinking about how the calculator would work. I needed a place to output my results, and I’d be using it over and over, so I wanted to only query it once, then save it to a variable. I had a paragraph in my HTML with an ID of “output”, so I declared:

const output = document.getElementById("output");

I also needed variables for the input numbers. Because the user enters a number, then an operator, then a second number, I would need two variables for storing the two numbers. In addition, when a user enters an operator, that operator isn’t used until the second number comes in, so that would also have to be saved until needed. I would also need a variable to hold the calculation result for output to the user:

let numOut = "", //number input before operator
numOutNew = "", //number input after operator
op = "", //operator
int = 0; //calculation result;

Now I was ready to write the “handleClick” function I had assigned to each key at the beginning of my script. It needed to differentiate between number keys and function keys. While numbers were being keyed in they could be added to the “numOut” variable for output. Because they were strings (HTML text node inside the buttons), adding would concatenate them rather than actually performing addition. This was handy because when you input a sequence of numbers into a calculator you are building a multi-digit number. For example, if you type in “1” then “2” you expect to see “12”, not “3”. If an operator key was pressed I needed to start calculating, so I assigned that a function, “doCalc()”, passing the function the value of the operator key’s inner text, which would be “+” or “*”, for example.

function handleClick() {
let num = this.innerText;
if (this.classList.contains("number")) {
numOut += num;
output.innerText = numOut;
} else {
doCalc(num);
}
}

I knew I would need to clear the output both when the “C” key was pressed and in the event of a bad operation, such as attempted division by zero, or if the user hit an operation key without inputing a number. I needed to clear the output and reset all the variables. Since I was going to reuse it I made it a function I could call:

function doClear() {
output.innerText = "";
numOut = "";
numOutNew = "";
op = "";
int = 0;
}

The first part, handling the clear button, was easy with my “doClear” function:

function doCalc(calc) {
if (calc === "C") {
doClear();
}

The next part was harder, because I needed to know whether the user had input more than one number. It might seem that you would only need to look at two inputs, the number before the operator and the one after, and only do an operation when you had the “=” being clicked. But in reality, a calculator chains operations. For example, you could input 1 + 2 * 3 / 4 + 6 and the calculator would have to keep generating a running total. The user might never actually click the “=” key, but they would be expecting a total every time they entered an operator and another number.

I also would not be using the current operator but the one entered previously, that is before the second number was entered. For example, if the user enters “1”, then “+”, I can’t add yet because I don’t have two numbers, so I have to store both the first number and the operator and wait for more input. If the user then enters “2” that number gets stored in a variable, and then if the user enters another operator key, for example “/”, I take the first number and add it to the second number and output the result. I save that result as the new first number for the “/” operation that is coming next and wait for the new second number.

I had created the variable “numOutNew” at the beginning of my script and gave it an empty value. It is used to save the first number for a calculation once I’ve received an operator. (It gets assigned a value when the “doCalc” script runs.) If it is empty it means I am getting my first operator, I’m running “doCalc” for the first time, and I don’t yet have a second number. I can’t do the math until I get the second number. Thus checking for a value for “numOutNew” in an “if” statement will determine if I should calculate or just save the operator until I get the second number:

else if (numOutNew !== "") {

Since I have five operators to handle I need a switch statement. I start by converting my first number from a text string to a number. Because JavaScript is a loosely-typed language it will change numeric strings to numbers when it can if asked to do a math operation on them, but this doesn’t apply to addition. If it is presented with a string and a “+” it will concatenate rather than add, so I’d get “12” if I tried to add 1 + 2 as strings. So I use “parseFloat” to make it a number:

numOut = parseFloat(numOut);

If I have a value for “numOutNew” that means I’ve run “doCalc” before, I have a saved operation, I’ve gotten a second number, and now a second operator has been passed. That means it’s time to do the math. I pass my saved operator from the previous time this function ran to my switch statement and take care of the math operation. I use the variable “int” that I created at the beginning of my script to hold the result of my calculation:

switch (op) {
case "/":
int = numOutNew / numOut;
break;
case "*":
int = numOutNew * numOut;
break;
case "-":
int = numOutNew - numOut;
break;
case "+":
int = numOutNew + numOut;
break;

Next I add the case for the “equals” operator. Remember that when an operator is input it triggers a calculation using the previous operator. For that reason, the “=” operator doesn’t actually perform a calculation. Instead, it only has to store the value of the previous calculation in case the user decides to chain additional calculations instead of clearing. That total will already have been passed to the output area of the calculator and I can pick it up from there:

case "=":
int = parseFloat(output.innerText);
break;

Because division by zero is undefined, I have to make sure that doesn’t happen, so I add a check for it in my “/” case:

case "/":
if (numOut !== 0) {
int = numOutNew / numOut;
} else {
int = "error";
}
break;

Notice that “int” started out as a numeric variable because it was initialized to zero, and it is being passed a number as the result of a math operation. But it can also be passed a string (“error”), because JavaScript variables are not locked into types the way most other variables are.

Now that I’m done setting up my math operations I have to store my results so I can send them to the calculator and use them in the next operation:

output.innerText = int; //Show result to user
numOutNew = int; //Save the value for chained calculations

Unfortunately, as is usually the case with JavaScript, things can go wrong. For example, what happens if the user enters the decimal point followed by an operator? The decimal is categorized as a number, but you can’t do a calculation with it alone. There could also be an error if the user input two consecutive operators. The switch statement expects every operator to be preceded and followed by a number, and if it only gets one number it produces a calculation whose result is not a number, or “NaN”. When this happens the calculator produces an error that must be relayed to the user, and the bad calculation result must be discarded. That means I have to add this as a condition to the output code (above). If there is an error I show the user an error message for two seconds, then clear the calculator. Otherwise I show the output:


if (isNaN(int)) {
output.innerText = "error";
setTimeout(function() {
doClear();
}, 2000);
} else {
output.innerText = int;
numOutNew = int;
}

The first part of this function handled calculations when two numbers were present. If not, I need an “else”. If I didn’t clear out my results due to a “NaN” output I will have a numeric value I can store to use as the first number in my next calculation:

else if (numOut !== "") {
numOutNew = parseFloat(numOut);
}

After these conditions have been evaluated I save the math operator that I passed in as “calc” to my function in the variable “op”, where it will be stored until the next number comes in and can be used. Remember, I don’t do the math operation that is passed into the “doCalc” function; I do the operation that was passed in previously. I empty my “numOut” variable so it’s ready to be used again by the next number the user inputs. This ends the “doCalc” function:

op = calc;
numOut = "";
}

Now that my script is finished I wrap it in an Immediately Invoked Function Expression (IIFE) so I don’t have to worry about global variables and conflicts. The completed code is in the pen below.

[codepen_embed height=”460″ theme_id=”21122″ slug_hash=”QWWXpvJ” default_tab=”result” user=”pixelslave”]See the Pen Shrink Text by Connie Finkelman (@pixelslave) on CodePen.[/codepen_embed]


Posted

in

,

by

Tags: