Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <script src="https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js"></script>

<!-- 

Hello Camper!

For now, the test suite only works in Chrome! Please read the README below in the JS Editor before beginning. Feel free to delete this message once you have read it. Good luck and Happy Coding! 

- The freeCodeCamp Team 

-->
<main></main>
              
            
!

CSS

              
                @import 'https://fonts.googleapis.com/css?family=Share+Tech+Mono';
@font-face {
      font-family: "Digital";
      src: url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.eot");
      src: url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.eot?#iefix") format("embedded-opentype"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.woff2") format("woff2"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.woff") format("woff"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.ttf") format("truetype"), url("//db.onlinewebfonts.com/t/8e22783d707ad140bffe18b2a3812529.svg#Digital-7") format("svg");
}

* {
    box-sizing: border-box;
    font-size: 30px;
}

body {
    width: 25%;
    min-width: 250px;
    max-width: 350px;
    margin: 0 auto;
}

#calculator {
    position: relative;
    left: -11%;
    font-family: Arial, sans-serif;
    box-sizing: content-box;
    margin-top: 2em;
    display: flex;
    flex-flow: column nowrap;
    border: outset 4px #49494b;
    border-radius: 0.5em;
    width: 100%;
    height: 25vh;
    min-height: 400px;
    padding: 1em;
    justify-content: space-between;
    background-color: #282828;
}

#screens {
    font-family: digital;
    flex: 1;
    display: flex;
    width: 100%;
    padding: 0 0.3em;
    flex-flow: column nowrap;
    margin-bottom: 0.5em;
    background-color: #a2af77;
    border: inset 3px #666;
    border-radius: 0.15em;
}

#screens label {
    display: flex;
    overflow: hidden;
    text-overflow: ellipsis;
    align-items: center;
}

#formula {
    flex: 1 1.5em;
    font-size: 0.75em;
}

#display {
    flex: 1 2em;
    font-size: 1em;
}

#buttons {
    flex: 5;
    display: grid;
    width: 100%;
    grid-template-columns: repeat(4, minmax(10px, 1fr));
    gap: 0.5em;
}

#buttons button {
    padding: 0.1em;
    text-align: center;
    color: white;
    background-color: #58595b;
    border: outset 2.5px #7c7d7f;
    border-radius: 0.15em;
    box-shadow: 2px 2px 3px 3px #1e1c1f;
}

#buttons button:focus {
    outline: none;
}

#buttons button:active {
    outline: none;
    border-style: inset;
    box-shadow: 0 0 0 0 #1e1c1f;
}

#buttons #clear,
#buttons #zero {
    grid-column: 1 / 3;
}

#buttons #clear {
    background-color: #ac3939;
    border-color: #c05353;
}

#buttons #equals {
    grid-column: 4 / 5;
    grid-row: 4 / 6;
    background-color: #f15a2b;
    border-color: #ff6e3f;
}
              
            
!

JS

              
                /* MAIN COMPONENT */
class App extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
            currentOperation: "",
            currentChar: "0",
            formula: "",
            tokenisedFormula: [],
            maxCharLimit: 15
        };
        
        this.handleButtonClick = this.handleButtonClick.bind(this);
        
        this.buttons = [
            {text: 'AC', id: 'clear'},
            {text: '/', id: 'divide'},
            {text: '*', id: 'multiply'},
            {text: '7', id: 'seven'},
            {text: '8', id: 'eight'},
            {text: '9', id: 'nine'},
            {text: '-', id: 'subtract'},
            {text: '4', id: 'four'},
            {text: '5', id: 'five'},
            {text: '6', id: 'six'},
            {text: '+', id: 'add'},
            {text: '1', id: 'one'},
            {text: '2', id: 'two'},
            {text: '3', id: 'three'},
            {text: '0', id: 'zero'},
            {text: '.', id: 'decimal'},
            {text: '=', id: 'equals'}
        ];
    }
    
    handleButtonClick(event) {
        const char = event.target.textContent;
        
        this.setState(processCharacter(char, this.state));
    }
    
    render() {
        const buttons = this.buttons.map((elem) => {
            return <Button
                       id={elem.id}
                       key={`button-${elem.text}`}
                       text={elem.text}
                       handleClick={this.handleButtonClick}
                   />;
        });
        
        return (
            <div id="calculator">
                <Screen formula={this.state.formula} current={this.state.currentChar} />
                <div id="buttons">{buttons}</div>
            </div>
        );
    }
}

ReactDOM.render(<App />, document.querySelector('main'));
/* END MAIN COMPONENT */

/* REACT COMPONENTS */
function Button(props) {
    return (
        <button id={props.id} key={props.key} onClick={props.handleClick}>{props.text}</button>
    );
}

function Screen(props) {
    return (
        <div id="screens">
            <label id="formula">{props.formula}</label>
            <label id="display">{props.current}</label>
        </div>
    );
}
/* END REACT COMPONENTS */

/* CONSTANTS */
// Types: Various strings that represent the different types of buttons
// that exist on a calculator
const OP = "operator";
const DIGIT = "digit";
const DECIMAL = "decimal";
const EQUALS = "equals";
const CLEAR = "clear";
const MINUS = "minus";

// Operator Tokens: Various strings that will be used as tokens to represent
// the different mathematical operations in the string format of the formula
// which will be parsed in the evaluation process
const OP_NEG = "negative";
const OP_MUL = "multiply";
const OP_DIV = "divide";
const OP_PLUS = "add";
const OP_MINUS = "subtract";

// Tokens Table: Maps the different mathematical operations to an array that
// includes various combinations of the tokens defined above. It uses 2 different
// tokens for the '-' character depending on where it is applied. If it comes
// after a mathematical operator, then it is the negative sign. If it comes
// alone then it is the subtraction operator.
const TOKENS = {
    "+": [OP_PLUS],
    "-": [OP_MINUS],
    "*": [OP_MUL],
    "/": [OP_DIV],
    "+-": [OP_PLUS, OP_NEG],
    "--": [OP_MINUS, OP_NEG],
    "*-": [OP_MUL, OP_NEG],
    "/-": [OP_DIV, OP_NEG]
};

// Tokens To Operations: Maps the tokens to the operation that will be executed
// when each token is encountered in the formula in its string format
const OPERATIONS = {
    [OP_NEG]: (x) => -x,
    [OP_MUL]: (x, y) => x * y,
    [OP_DIV]: (x, y) => x / y,
    [OP_PLUS]: (x, y) => x + y,
    [OP_MINUS]: (x, y) => x - y
};
/* END CONSTANTS */

/* INPUT PROCESSING */
function processCharacter(char, state) {
    /*
        This is the main entry point for the calculator logic. It takes a character
        as input from the user and processes it based on its type using various
        utility functions defined later.
        
        In general, the calculator logic is separated from the React components
        because it doesn't rely on React to actually work. It can work on its
        own for a console calculator or be used by other front-end frameworks
        libraries as well as with regular HTML, CSS and JS.
    */
    switch (getCharBaseType(char)) {
        case OP: {
            if (state.formula.length < state.maxCharLimit || formulaEvaluated(state.formula)) {
                return processOperator(char, state);
            }
            
            break;
        }
        case DIGIT: {
            if (state.formula.length < state.maxCharLimit || formulaEvaluated(state.formula)) {
                return processDigit(char, state);
            }
            
            break;
        }
        case DECIMAL: {
            if (state.formula.length < state.maxCharLimit || formulaEvaluated(state.formula)) {
                return processDecimal(char, state);
            }
            
            break;
        }
        case EQUALS: {
            return processEquals(char, state);
        }
        case CLEAR: {
            return {
                currentOperation: "",
                currentChar: "0",
                formula: "",
                tokenisedFormula: []
            };
        }
        default: {
            return {};
        }
    }
}

// Utilities
function processOperator(operator, state) {
    /*
        Processes mathematical operators displaying them in the UI for both
        the current character and the formula.
        
        It also adds the tokens for any numbers that were in the current
        character display because once the user presses one of the operators,
        it indicates that the previous operand is now complete and ready to
        added to the tokenised formula.
    */
    
    // Get the current state
    let currentOperation = state.currentOperation;
    let currentChar = state.currentChar;
    let formula = state.formula;
    let tokenisedFormula = state.tokenisedFormula;

    // A boolean value that will be used to determine what to do with the operator
    // depending on whether it is a single operator or a double where a minus is
    // the second one to make the following number negative
    let singleOpTest = true;

    if (formulaClear(formula)) {
        // If the user presses one of the operators with no numbers before, just
        // add 0 at the beginning
        tokenisedFormula = addToken("0", tokenisedFormula);
        
        formula += "0" + operator;
    } else if (formulaEvaluated(formula)) {
        // If the user presses an operator with a formula that has already been
        // evaluated, take the result of the formula and apply the operation to
        // it
        tokenisedFormula = addToken(currentChar, tokenisedFormula);
        
        formula = currentChar + operator;
    } else {
        if (decimalAtEnd(formula)) {
            // If an operator is pressed when the current number has a decimal
            // point at the end with no digits after it, add a 0 before adding
            // the operator to the formula so the number looks like this '3.0'
            // instead of this '3.'
            const token = currentChar + "0";
            
            tokenisedFormula = addToken(token, tokenisedFormula);
            
            formula += "0" + operator;
        } else if (digitAtEnd(formula)) {
            tokenisedFormula = addToken(currentChar, tokenisedFormula);
            
            formula += operator;
        } else {
            // If an operator is already the last character the user pressed
            // we determine what needs to be done. If there is only 1 operator
            // at the end and the user presses minus, then we add the minus
            // to that operator. If the user presses an operator other than
            // a minus, then we need to trim the last 1 or 2 characters, depending
            // on how many operators currently exist at the end, and replace
            // them with the new operator
            if (isMinus(operator) && singleOpAtEnd(formula)) {
                formula += operator;
                currentOperation += operator;
                singleOpTest = false;
            } else if (!isMinus(operator)) {
                if (singleOpAtEnd(formula)) {
                    formula = trimNCharsAtEnd(formula, 1);
                } else if (doubleOpAtEnd(formula)) {
                    formula = trimNCharsAtEnd(formula, 2);
                }

                formula += operator;
            }
        }
    }

    if (singleOpTest) {
        currentOperation = operator;
    }

    currentChar = operator;

    return {
        currentOperation,
        currentChar,
        formula,
        tokenisedFormula
    };
}

function processDigit(digit, state) {
    /*
        Processes digits adding them to the displays in the UI.
        
        It also tokenises the operators if a digit is pressed immediately
        after an operator because this indicates that the user has already
        settled on the operator s/he wants to apply.
    */
    
    // Get the current state
    let currentOperation = state.currentOperation;
    let currentChar = state.currentChar;
    let formula = state.formula;
    let tokenisedFormula = state.tokenisedFormula;

    if (formulaClear(formula) || formulaEvaluated(formula)) {
        // If the displays are clear or the formula has already been
        // evaluated, then just display the digit in the current and
        // formula displays in the UI
        currentChar = `${digit}`;
        
        if (digit !== '0') {
            formula = `${digit}`;
        }
    } else {
        // We check that the digit the user is pressing is not zero
        // or that there is no zero at the start of the current number
        // before we add the digit to the formula. This is to ensure
        // that the user cannot enter a number that has multiple zeroes
        // at the beginning (e.g 00005)
        if (digit !== '0' || !zeroAtStart(currentChar)) {
            if (singleOpAtEnd(formula) || doubleOpAtEnd(formula)) {
                // If the formula ends with an operator, just tokenise
                // it and replace current input display with the digit
                const token = tokeniseOperator(currentOperation);

                tokenisedFormula = addToken(token, tokenisedFormula);

                currentChar = `${digit}`;
            } else if (digitAtEnd(formula) || decimalAtEnd(formula)) {
                // If the formula ends with a digit, then add the digit
                // to the current input display
                currentChar += digit;
            }

            formula += digit;
        }
    }

    return {
        currentChar,
        formula,
        tokenisedFormula
    };
}

function processDecimal(decimal, state) {
    /*
        Processes the decimal point character.
        
        It also tokenises any operators that exist in the current
        input display if the decimal point is pressed immediately
        after an operator.
    */
    
    // Get the current state
    let currentOperation = state.currentOperation;
    let currentChar = state.currentChar;
    let formula = state.formula;
    let tokenisedFormula = state.tokenisedFormula;

    if (formulaClear(formula) || formulaEvaluated(formula)) {
        // If the user presses the decimal point at the start of
        // a formula or after a formula has been evaluated, then
        // add a 0 before adding the decimal point, so it looks
        // like this (0.) in the display rather than this (.)
        currentChar = formula = "0.";
    } else {
        if (singleOpAtEnd(formula) || doubleOpAtEnd(formula)) {
            // If the user presses the decimal point immediately
            // after an operator then add a 0 before adding the
            // decimal point, so it looks like this (0.) in the
            // display rather than this (.)
            // Additionally, tokenise the operator and add it to
            // the tokenised formula
            const token = tokeniseOperator(currentOperation);

            tokenisedFormula = addToken(token, tokenisedFormula);

            currentChar = "0.";
            formula += "0.";
        } else if (digitAtEnd(formula) || decimalAtEnd(formula)) {
            // If the current input display has a number in, make
            // sure that it doesn't already has a decimal point.
            // This prevents users from adding multiple decimal
            // points which would make the number invalid
            if (!hasDecimalPoint(currentChar)) {
                currentChar += ".";
                formula += ".";
            }
        }
    }

    return {
        currentChar,
        formula,
        tokenisedFormula
    };
}

function processEquals(equals, state) {
    /*
        This evaluates the formula when the user presses equals
        and calls the necessary functions to clear the tokens so
        the calculator is ready for subsequent calculations
    */
    
    // Get the current state
    let currentChar = state.currentChar;
    let formula = state.formula;
    let tokenisedFormula = state.tokenisedFormula;
    let result;

    // Perform some cleanup before evaluating the formula. If
    // the user pressed the equals button immediately after
    // a decimal point, then just add a 0 so the formula ends
    // with '.0' instead of '.'
    // If the user pressed '=' immediately after an operator
    // then remove the last 1 or 2 characters depending on how
    // many operators are there at the end. This is to ensure
    // that the formula will be evaluated successfully, as an
    // operator at the end of the formula wouldn't have an
    // operand which would produce errors when evaluating
    if (decimalAtEnd(formula)) {
        currentChar += "0";
        formula += "0";
    } else if (singleOpAtEnd(formula) || doubleOpAtEnd(formula)) {
        const trimLength = singleOpAtEnd(formula) ? 1 : 2;
        formula = trimNCharsAtEnd(formula, trimLength);
    }

    // If the formula ends with a number, make sure to add it
    // to the tokenised formula, since it wouldn't have been
    // added yet up to this point
    if (digitAtEnd(currentChar)) {
        tokenisedFormula = addToken(currentChar, tokenisedFormula);
    }

    // Checks to make sure that the formula is not already
    // evaluated. This prevents any extra recalculation if
    // the user presses '=' multiple times
    if (!formulaEvaluated(formula)) {
        result = evaluateFormula(tokenisedFormula);
    }

    // When the formula gets evaluated the 'result' variable
    // will be updated with a value. In this case, display it
    // properly in the UI.
    // Currently, this slices the result if its length exceeds
    // the maxmium number defined by the state. There might be
    // a better way of addressing this issue rather than
    // discarding the extra digits at the end.
    if (result) {
        formula += "=" + result;
        currentChar = (result.length < state.maxCharLimit) ? `${result}` : `${String(result).slice(0, state.maxCharLimit)}`;
    }

    tokenisedFormula = clearTokens();

    return {
        currentChar,
        formula,
        tokenisedFormula
    };
}

function addToken(token, tokenisedFormula) {
    const output = tokenisedFormula.concat(token);

    return output;
}

function clearTokens() {
    return [];
}

function tokeniseOperator(operator) {
    /* Convert the mathematical operators to their tokens */
    return TOKENS[operator] ? TOKENS[operator] : undefined;
}

function trimNCharsAtEnd(str, N) {
    return str.slice(0, str.length - N);
}
/* END INPUT PROCESSING */

/* EVALUATION */
function evaluateFormula(tokenisedFormula) {
    /*
        This is the main evaluation function. It takes the tokenised
        formula and executes the mathematical operations in the proper
        order, updating the tokenised formula along the way.
        
        If at the end, it has a single value, then the evaluation
        was successful, so it returns the result. Otherwise, it returns
        NaN.
        
        The order in which the evaluation goes:
            1) Start with negation, going from start to end of the
               formula negating any number preceeded by the negation
               token.
            2) Then, it moves to multiplication and division, going
               from start to end of formula applying these operations
            3) Finally, it applies the addition and subtraction.
    */
    tokenisedFormula = executeOperations(tokenisedFormula, [OP_NEG]);
    tokenisedFormula = executeOperations(tokenisedFormula, [OP_MUL, OP_DIV]);
    tokenisedFormula = executeOperations(tokenisedFormula, [OP_PLUS, OP_MINUS]);

    return tokenisedFormula.length === 1 ? tokenisedFormula[0] : NaN;
}

// Utilities
function executeOperations(formula, listOfOps) {
    let output = formula;
    
    // A function that returns true or false checking if the
    // list of operations to be executed includes the operator
    // passed to the function. This function will be used with
    // the findIndex Array method to get all the operators
    // that should be executed
    const opTest = (elem) => listOfOps.includes(elem);
    
    while (output.findIndex(opTest) !== -1) {
        const opIndex = output.findIndex(opTest);
        const operator = output[opIndex];
        
        // The args list will include the operands in the order
        // they should be evaluated. For example, if the formula
        // is 2 - 3, then the args list will be [2, 3]
        let args = [];
        
        // The followin part determines whether the operation to
        // be executed is unary (e.g. negation) or binary. Based
        // on that, it defines multiple constants and variables
        // that will be used to update the tokenised formula
        
        // The slice boundary defines how many items will be
        // removed from the tokenised formula before the operator
        // that is currently being executed. If the operation is
        // unary, then no items before the operator will be removed.
        // When the operation is binary, then 1 item will be removed.
        const sliceBoundary = OPERATIONS[operator].length === 2 ? 1 : 0;
        
        // We only need to defined a firstOperand if the operation
        // is binary
        let firstOperand;
        
        // The last operand is the item that comes after the operator
        const lastOperand = Number(output[opIndex + 1]);

        if (OPERATIONS[operator].length === 2) {
            // If the operation is binary, then define the first
            // operand as the one before the operator in the
            // tokenised formula, and add it to the args list
            firstOperand = Number(output[opIndex - 1]);

            args = [firstOperand];
        }

        // Add the lastOperand to the args list. This has to be done
        // here to make sure that, if the operation is binary, the
        // first operand has already been added to the list.
        args = [...args, lastOperand];
        
        // Update the tokenised formula, removing the tokens for the
        // operation that was executed and replacing them with the
        // result of the operation.
        output = [
            ...output.slice(0, opIndex - sliceBoundary),
            OPERATIONS[operator](...args),
            ...output.slice(opIndex + 2)
        ];
    }

    return output;
}
/* END EVALUATION */

/* INPUT PROCESSING TESTS */
function zeroAtStart(str) {
    return /^0$/.test(str);
}

function digitAtEnd(str) {
    return /\d$/.test(str);
}

function decimalAtEnd(str) {
    return /\.$/.test(str);
}

function singleOpAtEnd(str) {
    return /\d[\+\-\*\/]$/.test(str);
}

function doubleOpAtEnd(str) {
    return /\d[\+\-\*\/]\-$/.test(str);
}

function hasDecimalPoint(str) {
    return /^\d*\.\d*$/.test(str);
}

function formulaEvaluated(str) {
    return /\d+\=-?\d+\.?\d*$/.test(str);
}

function formulaClear(str) {
    return !str.trim();
}
/* END INPUT PROCESSING TESTS */

/* CHARACTER IDENTITY TESTS */
function getCharBaseType(char) {
    if (isOperator(char)) {
        return OP;
    } else if (isDigit(char)) {
        return DIGIT;
    } else if (isDecimal(char)) {
        return DECIMAL;
    } else if (isEquals(char)) {
        return EQUALS;
    } else if (isClear(char)) {
        return CLEAR;
    } else {
        return undefined;
    }
}

// Utilities
function isOperator(char) {
    return /[\+\-\*\/]/.test(char);
}

function isDigit(char) {
    return /\d/.test(char);
}

function isDecimal(char) {
    return /\./.test(char);
}

function isEquals(char) {
    return /\=/.test(char);
}

function isClear(char) {
    return /^AC$/.test(char);
}

function isMinus(char) {
    return /\-/.test(char);
}
/* END CHARACTER IDENTITY TESTS */
              
            
!
999px

Console