Javascript Calculator

Intro

This is the fourth project in the freeCodeCamp Front End Development Libraries Certification. The objective is to build a simple javascript calculator. It should do addition, subtraction, multiplication and division. The formula and result should be displayed on the screen. The suggested method is to use a front end library like React. Read more about the project at Build a Javascript Calculator

Check out the end product at https://calculator.projects.yasakdogra.com

Planning

We will use React, Redux and Tailwind CSS for this project. Check out how to setup a basic project at: React with Redux, Tailwind and More

We will create a calculator state using Redux. We will use key press actions to store user input and do the calculation when necessary and then use the state to show the output on the calculator display.

We will have a App component which will contain the Calculator component. The Calculator component will have Inputs and Screen components. The Inputs will have multiple instances of Key component. The Screen will have FormulaDisplay which will show the complete formula and the ActiveDisplay which shows the current input value.

State

Create src/state/calculatorSlice.ts with Redux imports

import { PayloadAction, createSlice } from "@reduxjs/toolkit";

Let’s define some types. We will need to check each key pressed by the user to make decisions in the action handlers.

The operands will be floating point numbers. We will create the type CalculatorOperand an array that has the numbers 0-9 in string format and the period(.) for decimal.

NOTE: The operand type can also be the union number | '.', but our key click inputs will send string values, its easier to just use an array with string values

The operators will be addition, subtraction, multiplication and division. We will create the type CalculatorOperator from an array that has the symbols/keys for the operators.

The rest of the keys will be calculator actions. We will create the type CalculatorAction from an array that has the symbols/keys for the actions.

We will create the type CalculatorInput that matches all inputs by creating a union of the three types above.

We will create the type CalculatorElement to store the calculator inputs, parsed numbers and the calculated results.

In addition to the types, we will also create functions to recognize which type the user input is. They will also serve as type guards for our code

const CALCULATOR_OPERANDS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.'] as const;
const CALCULATOR_OPERATORS = ['+', '-', 'x', '÷'] as const;
const CALCULATOR_ACTIONS = ['AC', 'DEL', '=', 'I', 'F'] as const;

type CalculatorOperand = typeof CALCULATOR_OPERANDS[number];
type CalculatorOperator = typeof CALCULATOR_OPERATORS[number];
type CalculatorAction = typeof CALCULATOR_ACTIONS[number];
export type CalculatorInput = CalculatorOperand | CalculatorOperator | CalculatorAction;
export type CalculatorElement = CalculatorInput | number;

export const isCalculatorOperand = (object: any): object is CalculatorOperand => CALCULATOR_OPERANDS.includes(object as CalculatorOperand);
export const isCalculatorOperator = (object: any): object is CalculatorOperator => CALCULATOR_OPERATORS.includes(object as CalculatorOperator);
export const isCalculatorAction = (object: any): object is CalculatorAction => CALCULATOR_ACTIONS.includes(object as CalculatorAction);

Create a type for calculator state and initialize the initial state. formula is a list of elements, current is the string to be displayed in the ActiveScreen component and method is the mode of the calculator. The mode can be either formula or intermediate. Formula mode calculates using BEDMAS whereas the intermediate calculates left to right.

Formula mode: 2 + 4 ÷ 2 = 4
Intermediate mode: 2 + 4 ÷ 2 = 3

interface CalculatorState {
    formula: CalculatorElement[];
    current: string;
    method: string;
}

const initialState: CalculatorState = {
    formula: [],
    current: '0',
    method: 'formula',
};

Create action creators

export const calculatorSlice = createSlice({
    name: 'formula',
    initialState,
    reducers: {
        keyPress(state, action: PayloadAction<CalculatorInput>) {
            if (isCalculatorOperator(action.payload)) {...}
            else if (isCalculatorAction(action.payload)) {...}
            else {...}
        }
    },
});

The if blocks for handling operators and calculator actions are pretty straight forward.

When a key with operator is clicked, it will parse the string in current an add it to formula. When an operator is clicked after a calculation is done, it will clear all the entries in formula and add the previous answer as the first element in formula. There is a special case is when there negative operator is clicked. The negative can follow another operator once to indicate that the next number is a negative number.

AC will clear the states, DEL will delete the last digit in current until 0 is reached, I and F toggle the intermediate and formula mode. = calls the calculate function with a copy of the formula and add the output to the formula after an equal sign.

These are the functions for calculating the answer

const calculate = (formula: CalculatorElement[], method: string): number => {
    // Replace "operator, negative sign, number" with "operator, negative number"
    for (let i = 0; i < formula.length; i++) {
        if (isCalculatorOperator(formula[i]) && formula[i + 1] === '-') {
            formula[i + 2] = -1 * (formula[i + 2] as number);
            formula.splice(i + 1, 1);
        }
    }
    
    if (method === 'intermediate') {
        return calcIntermediate(formula);
    }
    return calcFormula(formula);
}
const calcIntermediate = (formula: CalculatorElement[]): number => {
    let result = formula[0] as number;
    for (let i = 1; i < formula.length - 1; i++) {
        let curr = formula[i];

        if (isCalculatorOperator(curr)) {
            switch (curr) {
                case '+':
                    result += formula[i + 1] as number;
                    break;
                case '-':
                    result -= formula[i + 1] as number;
                    break;
                case 'x':
                    result *= formula[i + 1] as number;
                    break;
                case '÷':
                    result /= formula[i + 1] as number;
                    break;
            }
        }
    }

    return result;
}
const calcFormula = (formula: CalculatorElement[]): number => {
    // Calculate multiplication and division
    for (let i = 1; i < formula.length - 1; i++) {
        let curr = formula[i];

        if (isCalculatorOperator(curr)) {
            switch (curr) {
                case 'x':
                    formula[i + 1] = (formula[i - 1] as number) * (formula[i + 1] as number);
                    formula.splice(i - 1, 2);
                    i--;
                    break;
                case '÷':
                    formula[i + 1] = (formula[i - 1] as number) / (formula[i + 1] as number);
                    formula.splice(i - 1, 2);
                    i--;
                    break;
            }
        }
    }

    // Calculate addition and subtraction
    for (let i = 1; i < formula.length - 1; i++) {
        let curr = formula[i];

        if (isCalculatorOperator(curr)) {
            switch (curr) {
                case '+':
                    formula[i + 1] = (formula[i - 1] as number) + (formula[i + 1] as number);
                    formula.splice(i - 1, 2);
                    i--;
                    break;
                case '-':
                    formula[i + 1] = (formula[i - 1] as number) - (formula[i + 1] as number);
                    formula.splice(i - 1, 2);
                    i--;
                    break;
            }
        }
    }

    return formula[0] as number;
}

Components

The App, Calculator and Screen components are wrappers components with some styling using Tailwind classes. Let’s get into the components that use the state.

ActiveDisplay component

This component shows the current value from the state

Create src/components/ActiveDisplay.tsx

import { useSelector } from "react-redux";
import { RootState } from "../state/store";

export default function ActiveDisplay() {
    const active = useSelector((state: RootState) => state.calculator.current);
    
    return (
        <div id="display" className="min-h-[3rem] w-full text-white text-right text-2xl">
            {active}
        </div>
    );
}

FormulaDisplay component

This component is similar to ActiveDisplay component and shows the formula part of the state

import { useSelector } from "react-redux";
import { RootState } from "../state/store";

export default function FormulaDisplay() {
    const formula = useSelector((state: RootState) => state.calculator.formula);
    return (
        <div className="min-h-[2rem] w-full text-amber-400 text-right text-xl break-words tracking-wide">
            {formula}
        </div>
    );
};

Inputs component

This is the component where we can define what keys to render. We will use an array to store the key properties. We can add the size (s=small, l=large) to make the key one or two column width. We can also add a tailwind color class.

We will loop over all the elements of KEYS in the render function and render a Key component for each. We also pass in a key click handler.

import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../state/store";
import { keyPress } from "../state/calculatorSlice";
import Key from "./Key";

const KEYS = [
    { id: "clear", text: "AC", size: "l", color: "bg-red-700" },
    { id: "backspace", text: "DEL", size: "s", color: "bg-stone-400" },
    { id: "divide", text: "÷", size: "s", color: "bg-stone-400" },
    { id: "seven", text: "7", size: "s" },
    { id: "eight", text: "8", size: "s" },
    { id: "nine", text: "9", size: "s" },
    { id: "multiply", text: "x", size: "s", color: "bg-stone-400" },
    { id: "four", text: "4", size: "s" },
    { id: "five", text: "5", size: "s" },
    { id: "six", text: "6", size: "s" },
    { id: "subtract", text: "-", size: "s", color: "bg-stone-400" },
    { id: "one", text: "1", size: "s" },
    { id: "two", text: "2", size: "s" },
    { id: "three", text: "3", size: "s" },
    { id: "add", text: "+", size: "s", color: "bg-stone-400" },
    { id: "zero", text: "0", size: "s" },
    { id: "decimal", text: ".", size: "s" },
    { id: "method", text: "I/F", size: "s", color: "bg-stone-400" },
    { id: "equals", text: "=", size: "s", color: "bg-stone-400" },
];

export default function Inputs() {
    const method = useSelector((state: RootState) => state.calculator.method);
    const dispatch = useDispatch<AppDispatch>();

    const handleKeyPress = (text: any) => {
        dispatch(keyPress(text));
    }

  return (
    <div className="grid grid-cols-4 gap-0.5">
        {KEYS.map((key) => {
            let text = key.text;
            let color = key.color;
            if(key.id === "method") {
                if(method === "formula") {text = "F"; color = "bg-green-500"}
                else text = "I";
            }
            return <Key key={key.id} id={key.id} text={text} size={key.size} color={color} handleKeyPress={handleKeyPress} />
        })}
    </div>
  );
};

Key component

We can now render the key component with the props passed to it

type KeyProps = {
    id: string;
    text: string;
    size?: string;
    color?: string;
    handleKeyPress: (id: string) => void;
};

export default function Key({ id, text, size, color, handleKeyPress }: KeyProps) {
    return (
        <button
            id={id}
            className={`${size === "l" ? "col-span-2" : "col-span-1"}
            ${color ? color : "bg-stone-500"}
            text-stone-200 text-2xl text-center h-14
            opacity-80 hover:opacity-100 hover:border-2`}
            onClick={() => handleKeyPress(text)}
        >
            {text}
        </button>
    );
};

Thank you for reading. You can also check out my other projects for this series below.