harunpehlivan
3/6/2018 - 7:11 PM

JavaScript Calculator

JavaScript Calculator

<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://codepen.io/U-ways/pen/qNLZrg" rel="stylesheet" />
/* Color Scheme
================================================================================
Main Colors:
#c5dec2 - #95a893
#3e5050 - #3a4949
#263331 - #333

Matching R&G:
#549042 - #8e0000
*//* App
==============================================================================*/
#app {
  font-family: sans-serif;
}

/* Calculator Container
==============================================================================*/
#calculator-con {
  width: 290px;
  min-height: 435px;
  position: relative;
  justify-content: flex-start;
  padding: 10px 0;
  background-color: #739595;
  box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.2);
  border-radius: 4%;
}

/* Display Section
==============================================================================*/
#display {
  height: auto;
  width: 260px;
  padding: 0 10px;
  margin-bottom: 10px;
  text-align: right;
  border-radius: 5%;
  border: solid rgba(0, 0, 0, 0.61) 2px;
  background-color: #CFE1D7;
  overflow: hidden;
}

/* Results Screen
============================================*/
#screen {
  font-size: 40px;
  color: #364545;
}

/* Tracker Screen
============================================*/
#tracker {
  margin-top: -5px;
  font-size: 20px;
  color: #3b4c4c;
  /**As there are no spacing between each formula,
  I had to break-word so they can be wrapped inisde the p**/
  text-align: left;
  word-wrap: break-word;
}

/* Keypad Layout
==============================================================================*/
#keypad {
  width: auto;
  text-align: center;
  position: relative;
} #keypad ul li {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width: 65px;
  height: 65px;
  margin: 1px;
  font-weight: bold;
  font-size: 35px;
  border-radius: 5%;
  list-style-type: none;
  user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  cursor: pointer;
}

/* Operator Keys
============================================*/
#operators {
  width: auto;
  flex-wrap: wrap;
  margin-bottom: 10px;
  padding: 0;
} #operators li {
  background: #3e5050;
  color: white;
  border: 1px solid rgba(255, 255, 255, 0.08);
  transition: all ease 0.2s;
} #operators li:active {
  box-shadow: inset  0px 2px 2px rgba(0, 0, 0, 0.2);
  background: #364747;
}

/* Number Keys
============================================*/
#numbers {
  width: 268px;
  flex-wrap: wrap;
}

.number {
  color: #3b4c4c;
  background: #c5dec2;
  transition: all ease 0.2s;
} .number:active {
  box-shadow: inset  0px 1.5px 2px rgba(0, 0, 0, 0.50);
}

/* Clear Button
============================================*/
#clear {
  background-color: #AC2C2C;
  color: white;
} #clear:active {
  box-shadow: inset  0px 4px 2px rgba(0, 0, 0, 0.30);
  background: #AC2C2C;
}

/* Decimal Button
============================================*/

#decimal {
  color: #3b4c4c;
  background: #c5dec2;
  transition: all ease 0.2s;
} #decimal:active {
  box-shadow: inset  0px 1.5px 2px rgba(0, 0, 0, 0.50);
}

/* Equal Button
============================================*/
#equal {
  width: 268px;
  height: 58px;
  margin-top: 10px;
  text-align: center;
  font-size: 50px;
  color: white;
  background: #416E36;
  border: 1px solid rgba(0,0,0,0.05);
  border-radius: 3%;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  transition: all ease 0.2s;
} #equal:active {
  box-shadow: inset  0 2px 2px rgba(0,0,0,0.18);
  background: #3b6331;
}
<script src="https://codepen.io/U-ways/pen/qNLZrg"></script>
/*jshint esversion: 6*/
/* Initial selector assignments
==============================================================================*/
const display   = document.querySelector('#screen');
const tracker   = document.querySelector('#tracker');
const numbers   = document.querySelectorAll('#numbers .number');
const operators = document.querySelectorAll('#operators li');

const decimalBtn = document.querySelector('#decimal');
const clearBtn   = document.querySelector('#clear');
const equalBtn   = document.querySelector('#equal');

/* Initial variable assignments
==============================================================================*/

/** @var memory
 * memory  will hold @var register & @var operator values when they're pushed.
 *
 * NOTE:
 * The @var register values will always be converted to float [parseFloat()]
 * When pushed to the memory.
 */
let memory = [];

/** @var history
 * history is used to track the user calculations.
 * It stores any valid input and displays them inside @const tracker.
 */
let history = '';

/** @var register
 * register stores number @type string and is pushed when:
 *  1.The user press the equal button =>
 *     (@const equalBtn @event calculate triggered)
 *  2.The user inputs an operator after inputting a number =>
 *     (@const operators @event operatorHandler triggered)
 */
let register = null;

/** @var operatorLocker
 * As the name indicates, operatorLocker locks operational fucntions:
 *  - @function operatorHandler()
 *  - @function calculate()
 *
 * This is done to avoid errors caused from clicking the operators twice.
 * (E.g. @const operators @event operatorHandler triggered twice.)
 *
 * The value will be true whenever the calculator is not expecting an operator.
 */
let operatorLocker  = true;

/** @var resultChanining
 * As I am allowing the chain of mathematical results, I am using the output of each
 * calculation made as a pushed register value. Thus, the calculator is expecting
 * an operator afterwards.
 *
 * However, if the user decided to not continue with the previous calculation, and start
 * over by pressing a key number. The calculator should reset.
 *
 * When a user presses a key number and the calculator is expecting a result chain,
 * resultChanining will trigger @function resetCalculator() when evaluated.
 */
let resultChanining = false;

/* Assign click listener & handlers for numbers & operators:
==============================================================================*/
equalBtn.addEventListener('click', calculate);
decimalBtn.addEventListener('click', decimalHandler);
clearBtn.addEventListener('click', resetCalculator);
operators.forEach(operator => operator.addEventListener('click', operatorHandler));
numbers.forEach(number => number.addEventListener('click', numberHandler));

/* numberHandler:                                           this#As_a_DOM_event_handler
==============================================================================*/
function numberHandler() {
  let number = this.getAttribute('data-value');
  if (!resultChanining) {
    register = (register !== null) ? register += number : number;
    history += number;
    tracker.innerHTML = history;
    display.innerHTML = register;
    operatorLocker = false;
  } else {
    resetCalculator();
  }
}

/* operatorHandler:
==============================================================================*/
/** @function operatorHandler()
 * As operators are used after inputting a number. operatorHandler() takes @var memory length
 * into considerations to assess each operational situation:
 *
 * - If memory.length === 0:
 *     User inputted a number before calling the operator.
 *     Parse and push that number from @var register,
 *     and then push @var operator to @var memory.
 *
 * - If memory.length === 1:
 *     resultChanining is true, the memory already have a number and the calculator is
 *     expecting @var operator only. Push @var operator to @var memory.
 *
 * - If memory.length === 2:
 *     There is already a number and an operator inside memory. Therefore,
 *     User inputted a number before calling the operator.
 *        Use the inputted number and find the result of both numbers:
 *          math(memory[0],memory[1],parseFloat(register)
 *        Push the @var result to @var memory, clear @var register,
 *        and then push the new @var operator to @var memory.
 *
 * Afterwards, sanitize some values and update the tracker.
 */
function operatorHandler() {
  if (!operatorLocker) {
    let operator = this.getAttribute('data-value');

    switch (memory.length) {
      case 0:
        register = parseFloat(register);
        memory.push(register);
        register = null;
        memory.push(operator);
        break;
      case 1:
        register = null;
        memory.push(operator);
        break;
      case 2:
        let result = math(memory[0],memory[1],parseFloat(register));
        memory   = [result];
        register = null;
        memory.push(operator);
        display.innerHTML = resultDisplay(result);
        break;
      default:
        resetCalculator();
        display.innerHTML = "Operator Error";
    }

    history  += operator;
    tracker.innerHTML = history;
    operatorLocker = true;
    resultChanining= false;
    decimalBtn.removeEventListener('click', decimalHandler);
    decimalBtn.addEventListener('click', decimalHandler);
  }
  console.log(memory);
}

/* calculate:
==============================================================================*/
/** @function calculate()
 * calculate will produce the output of two numbers if the
 * memory.length === 2 and the calculator is not expecting an operator.
 *
 * NOTE:
 * @const equalBtn is an operator itself, so if the user had inputted a number and
 * an operator (memory.length === 2), you still won't be able to use @function calculate()
 * to produce an output, as the @var operatorLocker is true from @function operatorHandler().
 *
 * A user needs to input another number so @function numberHandler will turn -
 * @var operatorLocker to false again.
 */
function calculate() {
  if (!operatorLocker && memory.length === 2) {
    let result = math(memory[0],memory[1],parseFloat(register));
    register = null;
    resultChanining = true;
    memory   = [result];
    display.innerHTML = resultDisplay(result);
    decimalBtn.removeEventListener('click', decimalHandler);
    decimalBtn.addEventListener('click', decimalHandler);
  }
  console.log(memory);
}

/* Result Display:
==============================================================================*/
/** @function resultDisplay(number)
 * Takes @var result to display it nicely on @const display as big numbers
 * will overflow the display screen.
 */
function resultDisplay(number) {
  number = roundNumber(number, 3);
  return (number >= 1e+8) ? number.toExponential(3) : number;
}

/* Rounding algorithm:                              http://stackoverflow.com/a/12830454
==============================================================================*/
/** @function roundNumber(num, scale)
 * A Rounding algorithm because JavaScript -- and most other programming languages --
 * have issues with float numbers display.
 */
function roundNumber(num, scale) {
  var number = Math.round(num * Math.pow(10, scale)) / Math.pow(10, scale);
  if(num - number > 0) {
    return (number + Math.floor(
      2 * Math.round(
        (num - number) * Math.pow(10, (scale + 1))
      ) / 10
    ) / Math.pow(10, scale));
  } else {
    return number;
  }
}

/* Math:
==============================================================================*/
function math(a,b,c) {
  let result = 0;
  switch (b) {
    case '/':
      result = (a/c);
      break;
    case '*':
      result = (a*c);
      break;
    case '+':
      result = (a+c);
      break;
    case '-':
      result = (a-c);
      break;
    default:
      resetCalculator();
      display.innerHTML = "Math Error";
  }
  return result;
}

/* decimalHandler:
==============================================================================*/
function decimalHandler() {
  this.removeEventListener('click', decimalHandler);
  if (!resultChanining) {
    register = (register !== null) ?  register+= '.' : '0.';
    history  = (register !== null) ?  history += '.' : '0.';
    display.innerHTML = register;
    tracker.innerHTML = history;
  } else {
    resetCalculator();
  }
}

/* resetCalculator:
==============================================================================*/
function resetCalculator() {
  memory          = [];
  history         = '';
  register        = null;
  operatorLocker  = true;
  resultChanining = false;
  display.innerHTML = 0;
  tracker.innerHTML = 'Standard';
  decimalBtn.removeEventListener('click', decimalHandler);
  decimalBtn.addEventListener('click', decimalHandler);
}

/* Adding project Details
==============================================================================*/
addProjDetails(0, 'Project Overview', 'This is a JavaScript calculator with a standard functionality. You can calculate and chain mathematical operations. There is a history panel to track your input and you\'re able to chain your previous results to preform further operations.');
addProjDetails(0, 'Techniques/Technologies', 'There is nothing special used to build this calculator. basic HTML, basic CSS, and basic JavaScript. However, lots of thought was put to make this calculator error free. While looking at other similar JavaScript calculators, I have seen many implementation which where prone to lots of errors and bugs. So I believe the actual challenge for this project was to make something functional and error-free.');

addProjDetails(1, 'Hurdles encountered', 'Due to the way float numbers are built in JavaScript. There is a rounding error that can\'t be completely avoided. I had to search around for a suitable rounding algorithm to help me reducing this margin of rounding error. <a href="https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html" target="_blank">Read more about floating-point arithmetic</a>...');
addProjDetails(1, 'Future implementations', 'I can add the modulus, root, or sign operator. But this is as simple as adding the other operators with a bit of mathematical considerations. This is not an important feature, so unless directly requested for educative purposes, it is not worth building.');
addProjDetails(1, 'Reflection', 'This project looks simple at first. But doing it right required lots determination.');

JavaScript Calculator

Build a JavaScript Calculator - freecodecamp Pure JavaScript standard calculator with history tracking and results chaining functionality.

A Pen by HARUN PEHLİVAN on CodePen.

License.

#container
  header#header.grid.cols-6
    section#proj-heading
      h1 JavaScript Calculator
    details#proj-details
      summary Project Info
      section#details-body.grid.cols-2.rows-2
        .details-column
        .details-column
  main#app.flex-c
    #calculator-con.flex-c
      section#display
        #screen 0
        p#tracker Standard
      section#keypad.flex-c
        ul#operators.flex-r
          li data-value="/"  &divide;
          li data-value="*"  &times;
          li data-value="+"  +
          li data-value="-"  -
        ul#numbers.flex-r
          li.number data-value="1"  1
          li.number data-value="2"  2
          li.number data-value="3"  3
          li.number data-value="4"  4
          li.number data-value="5"  5
          li.number data-value="6"  6
          li.number data-value="7"  7
          li.number data-value="8"  8
          li.number data-value="9"  9
          li.number data-value="0"  0
          li#decimal .
          li#clear C
        button#equal &#61;
  footer#footer
    p
      | Last Updated:
      time datetime="2017-05-03"  03/05/2017
      | &nbsp | &nbsp
      a href="https://codepen.io/hrunpehlivan" target="_blank" 
        i.fa.fa-codepen aria-hidden="true" 
        | HARUN PEHLİVAN