123

Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

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

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

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.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              <!-- <link rel="stylesheet" type="text/css" href="../bootstrap.css"/> -->
<head>
  <link rel="stylesheet" type="text/css" href="style.min.css"/>
  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.min.css">
</head>

<body>
  <div class="container">
    <div class="calculator">
      <!-- TITLE -->
      <h2 id="title">Electronic Calculator</h2>
      <!-- DISPLAY -->
      <div class="display">
        <p id="topDisplay">0</p>
        <p id="botDisplay">0</p>
      </div>
      <!-- BUTTONS -->
      <div class="buttons"> <!-- button order from topleft to bottom right-->
        <button value="AC">AC</button>
        <button value="CE">CE</button>
        <button value="÷">÷</button>
        <button value="x">X</button>
        <button value="7" class="num">7</button>
        <button value="8" class="num">8</button>
        <button value="9" class="num">9</button>
        <button value="-">-</button>
        <button value="4" class="num">4</button>
        <button value="5" class="num">5</button>
        <button value="6" class="num">6</button>
        <button value="+">+</button>
        <button value="1" class="num">1</button>
        <button value="2" class="num">2</button>
        <button value="3" class="num">3</button>
        <button value="=" id="equal-button">=</button> <!-- grid case -->
        <button value="0" class="num" id="zero-button" >0</button> <!-- grid case -->
        <button value=".">.</button>
      </div>
      <!-- end buttons-->
    </div>
    <!--end calculator -->
  </div>
  <!-- end container -->
  <div id="mocha"></div>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/chai/2.3.0/chai.min.js"></script>
  <script type="text/javascript" src="../jquery-3.2.1.min.js"></script>
  <script type="text/javascript" src="script.js"></script>
  <script type="text/javascript" src="script.test.js"></script>
</body>

            
          
!
            
              /*********************** MOCHA TDD STYLES ****************/

// Hide the annoyingly large stacktraces from mocha
.error {
  max-height: 25px !important;
}

/*********************** GLOBAL ****************/
.container {
  display: flex;
  justify-content: center;
}
h2#title {
  margin: 2px;
  text-align: center;
}
.calculator {
  padding: 10px;
  border: 2px solid black;
  border-radius: 10px;
  background-color: #dfd8d0; /* light pink */
  height: 340px;
}
.display {
  background-color: #c3c2ab; /* retro green */
  border-radius: 10px;
  border: 2px solid black;
  text-align: right;
  padding-right: 5px;
  #output {
    font-size: 20px;
  }
  #entry {
    color: grey;
  }
  p {
    margin: 0px;
  }
}

/*********************** BUTTONS ****************/
/* https://gridbyexample.com/examples/example19/ */
.buttons {
  display: grid;
  grid-template-columns: repeat(4,50px);
  grid-template-rows: repeat(5,20%);
  grid-gap: 10px;
  margin-top: 10px;

  button {
    padding: 5px;
    border-radius: 5px;
    font-size: 110%;
    background-color: black;
    color: white;
  }
  button[value="AC"],
  button[value="CE"]{
    background-color: #a72d45; /* dark red */
  }
  #equal-button {
    grid-column: 4/5;
    grid-row: 4/6;
  }
  #zero-button {
    grid-row: 5/6;
    grid-column: 1/3;
  }
}

            
          
!
            
              // https://stackoverflow.com/questions/5834318/are-variable-operators-possible
// Math library
var operations = {
  'x': function(a,b) { return b*a},
  '÷': function(a,b) { return b/a},
  '+': function(a,b) { return b+a},
  '-': function(a,b) { return b-a},
}
const isOper = /(-|\+|÷|x)/;

var util = {
  splitNumAndOper: function(rawString){
    // https://stackoverflow.com/questions/49546448/javascript-split-a-string-into-array-matching-parameters

    // Clean up data before Tokenization by applying Math Associative Property
    rawString = rawString.replace(/\-/, "+-");
    if(rawString.charAt(0) == "+"){
      rawString = rawString.substring(1);
    }

    // Tokenize operators from numeric strings
    let splitArray = rawString.split(/([^-0-9.]+)/);

    // Parse numeric tokens into floats to prevent string concatenation during calculation
    splitArray = splitArray.map(function(el){
      if($.isNumeric(el)){
        return parseFloat(el);
      } else {
        return el;
      }
    });

    return splitArray;
  },
  exceedDisplay: function(rawString){
    return (rawString.length > 9) ? true : false;
  },
  shuntyardSort: function(rawArr){
    if(!Array.isArray(rawArr)){
      console.error("shuntyardSort did not receive an Array");
    }

    let valueStack = [];
    let operStack = [];
    let isOperPushReady = false;
    const PEMDAS = {
      "x": 2,
      "÷": 2,
      "+": 1,
      "-": 1
    }

    // Convert infix to PEMDAS postfix
    rawArr.forEach(function(el,index,arr){
      if($.isNumeric(el)){ // We have a number
        valueStack.push(el);
        // Oper always adjacent to left and right num, this accounts for right num
        if(isOperPushReady){
          valueStack = valueStack.concat(operStack.reverse());
          operStack = [];
          isOperPushReady = false;
        }
      } else { // We have an operator
        operStack.push(el);
        // Need at least 2 oper to compare if current operator has higher precedence than previous
        if(operStack.length !== 1 && (PEMDAS[el] > PEMDAS[operStack.slice(-2)[0]])){
          isOperPushReady = true;
        }
      }
    });
    // Push remaining operators onto valuestack
    valueStack = valueStack.concat(operStack);
    return valueStack;
  },
  shuntyardCalc: function(rawArr){
    // Find first Operator except (-) because its reserved as a neg num not an operator anymore
    function findFirstOperator(element){
      return /(\+|÷|x)/.test(element);
    }

    if(!Array.isArray(rawArr)){
      console.error("shuntyardCalc did not receive an Array");
    }
    let infiniteLoopCounter = 0;
    let index = 0;
    let evalPartial = 0;
    let firstNum = 0;
    let secondNum = 0;
    let op = 0;

    /*
     * Calculate the postfix after Djikstras Shuntyard Sort Algo
     * By finding the first operator index, calculating operand + 2previous values
     * and pushing result back in
     * Repeat until everything is calculated
     */
    while(rawArr.length > 1){
      index = rawArr.findIndex(findFirstOperator);
      firstNum = parseFloat(rawArr.splice(index-1,1));
      secondNum = parseFloat(rawArr.splice(index-2,1));
      op = rawArr.splice(index-2,1);
      evalPartial = operations[op](firstNum, secondNum);
      evalPartial = Math.round(evalPartial * 10000000000)/10000000000;
      rawArr.splice(index-2,0, evalPartial);

      infiniteLoopCounter++;
      if(infiniteLoopCounter > 10){
        debugger;
      };
    }

    return rawArr.toString();
  },
  grabLastToken: function(rawStr){
    //https://stackoverflow.com/questions/49546448/javascript-split-a-string-into-array-matching-parameters
    return (rawStr == "" || rawStr == "0.") ? rawStr : rawStr.match(/\d+|[\+-\/x÷]/g).pop();
  }
}

var view = {
  render: function(cache,buttonValue){
    // Use placeholder vars for display to prevent 0 and "" confusion
    let topDisplay = util.grabLastToken(cache);
    let botDisplay = cache;

    if(buttonValue == "CE"){
      topDisplay = 0;
    }
    if(botDisplay == ""){
      botDisplay = 0;
    }
    if(topDisplay == ""){
      topDisplay = 0;
    }
    $('#topDisplay').html(topDisplay);
    $('#botDisplay').html(botDisplay);
    console.log(cache);
    console.log(typeof(cache));
  }
}

var model = {
  getAnswer: function(cache){
    return cache.split('=')[1];
  },
  pushDot: function(cache, lastCall){
    if(lastCall=="calculate" || cache ==""){
      cache = "0";
    }
    // Grab last numeric token and check if it has a "." in it
    return (cache.match(/[\d.]+$/g)[0].includes("."))
      ? cache : cache+".";
  },
  pushNumber: function(cache, buttonValue, lastCall) {
    return lastCall == "calculate" ? buttonValue : cache+buttonValue;
  },
  pushOperator: function(cache, buttonValue, lastCall){
    if(cache==""){
      return cache;
    }
    if(isOper.test(cache.slice(-1))){
      cache = cache.slice(0,-1);
    }
    return cache+buttonValue;
  },
  clearAll: function(cache, lastCall){
    return '';
  },
  clearEntry: function(cache, lastCall){
    //https://stackoverflow.com/questions/11134004/regex-that-will-match-the-last-occurrence-of-dot-in-a-string/
    // targets last operator +÷x- and its' remaining string .......replaces it with nothing
    // 1. (\+|÷|x|-)     Seek Operators.
    // 2. (?=            Conditional check....
    // 3. [^(\+|÷|x|\-)] For any other operators until end.
    // 4. *$)(.*)/       Grab everything after
    const lastEntry = /(\+|÷|x|-)(?=[^(\+|÷|x|\-)]*$)(.*)/;

    if (cache.includes("=")){
      cache = "";
    } else if(isOper.test(cache.slice(-1))){ // if lastchar is operator
      cache = cache.slice(0,-1); // delete
    } else if(isOper.test(cache)){ // If string has operator
      cache = cache.replace(lastEntry, '$1'); // remove numbers ahead
    } else { // no operators
      cache = '';
    }
    return cache;
  },
  calculate: function(cache, lastCall){
    if( isOper.test(cache.slice(-1)) || cache.slice(-1) == "."){
      return cache;
    }
    let tempArr = util.splitNumAndOper(cache);

    // Edsger Dijkstra - Shuntyard Algorithm
    tempArr = util.shuntyardSort(tempArr);
    tempArr = util.shuntyardCalc(tempArr);
    cache = cache + "=" + tempArr.toString();
    return cache;
  },
};

// Display, Read, Update, Destroy
// VIEWS + CONTROLLER IN JQUERY
$(document).ready(function(){
  let cache = '';
  let lastCall = 'clearAll'; // Assume last functionCall is a hard reset
  // Condense down into one click button
  $("button").on("click", function(){
    let buttonValue = $(this).attr("value");
    switch(buttonValue) {
      // Numbers
      case '.':
        cache = model.pushDot(cache, lastCall);
        lastCall = "pushDot";
        break;
      case '0':
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
        cache = model.pushNumber(cache, buttonValue, lastCall);
        lastCall = "pushNumber";
        break;
      case 'x':
      case '÷':
      case '-':
      case '+':
        cache = model.pushOperator(cache, buttonValue, lastCall);
        lastCall = "pushOperator";
        break;
      case 'AC':
        cache = model.clearAll(cache, lastCall);
        lastCall = "clearAll";
        break;
      case 'CE':
        cache = model.clearEntry(cache, lastCall);
        lastCall = "clearEntry";
        break;
      case '=':
        cache = model.calculate(cache, lastCall);
        lastCall = "calculate";
        break;
      default:
        console.log('ERROR DEFAULT CASE SHOULD NOT RUN!');
        break;
    }
    view.render(cache,buttonValue);

    if(lastCall == "calculate"){
      cache = model.getAnswer(cache);
    }
  });
});


// Unit Test Case

// MOCHA - test

// UI
mocha.setup('bdd')

mocha.setup({
	ui:'bdd',

})

// CHAI
var assert = chai.assert;
var expect = chai.expect;
var should = chai.should();

// Based on http://yeoman.io/contributing/testing-guidelines.html

describe('MODEL', function(){
	describe('getAnswer', function(){
		it('grab number token after =', function(){
			assert.equal("99",model.getAnswer("44+55=99"));
		})
	})

	describe("pushDot", () =>{
		it('forbid multiple "." for one token', () =>{
			assert.equal("9.99",model.pushDot("9.99"));
		})
		it('add dot if none present', () =>{
			assert.equal("999x9.",model.pushDot("999x9"));
		})
		it('add zero if empty cache', () =>{
			assert.equal("0.",model.pushDot(""));
		})
		it('reset to zero if calculate lastcall', () =>{
			assert.equal("0.",model.pushDot("999","calculate"));
		})
		it('limit one "." per token', function(){
			assert.equal("12.34+56.",model.pushDot("12.34+56"));
		})
	})
	describe("pushNumber", () =>{
		it("push number as a char", () =>{
			assert.equal("9",model.pushNumber("", 9));
		})
		it("concatenate as chars not add", () =>{
			assert.equal("99", model.pushNumber('9', '9'));
		})
		it('reset if lastCall is calculate', () =>{
			assert.equal("5",model.pushNumber("999","5","calculate"));
		})
	})

	describe("pushOperator", () =>{
		it('forbid sequential operators', () =>{
			assert.equal("999+555+", model.pushOperator("999+555+","+"));
		})
		it('forbid operators on empty cache', () =>{
			assert.equal("",model.pushOperator("","+"));
		})
		it('allow swappable operators', () => {
			assert.equal("123+", model.pushOperator("123-", "+"));
		})
	})

	describe("clearAll", () =>{
		it("clear everything", () =>{
			assert.equal("", model.clearAll("555+555"));
		})
	})

	describe("clearEntry", () =>{
		it("delete all if no operators", () =>{
			assert.equal("", model.clearEntry("5555"));
		})
	  it("delete operator if cache's last char", () =>{
			assert.equal("555",model.clearEntry("555+"));
		})
		it("delete number token before operator",() =>{
			assert.equal("555+",model.clearEntry("555+444"));
		})
		it('delete all if calculate lastcall', () =>{
			assert.equal("",model.clearEntry("5+5=10"));
		})
	})

	describe("calculate", () =>{
		it("do order of operations", () =>{
			assert.equal("5+5=10",model.calculate("5+5"));
		})
		it('handle 1 float calc',()=>{
			assert.equal("12.34+5=17.34", model.calculate("12.34+5"));
		})
		it('handle 2 float calc', () => {
			assert.equal("6.6+3.3=9.9", model.calculate("6.6+3.3"));
		})
		it('forbid incomplete operation', () => {
			assert.equal("6+", model.calculate("6+"));
		})
	})
}) // END MODEL
///////////////////////////////////////////////////////////
describe('VIEW', function(){
	describe("render", () =>{
		it('throw "Digit Limit Met" if lastNumSeq > 9 chars', () =>{
		})
		it('throw "Digit Limit Met" if calculation > 9 chars', () =>{
		})
		it('throw "Digit Limit Met" if cache > 26 char', () =>{
		})
		it('show 0 if cache is blank', () =>{
		})
		it('render curBuffer after Clearall or clearEntry', () =>{
		})
	})
	describe('render CACHE RESETS', () =>{
		it('return the number after "=" if it is present', () =>{
		})
	})
}) // END VIEW
///////////////////////////////////////////////////////////
describe('UTIL', function(){
	describe("splitNumAndOper", () =>{
		it('do simple math', () =>{
			assert.deepEqual([6,'+',4,'+',3], util.splitNumAndOper("6+4+3"));
		})
		it('tokenize negative numbers', () =>{
			assert.deepEqual([-1,'+',7], util.splitNumAndOper('-1+7'));
		})
		it('tokenize decimal numbers', function(){
			assert.deepEqual([12.34, '+', 5], util.splitNumAndOper('12.34+5'));
		})
	})

	describe('shuntyardSort', () =>{
		it('convert infix to sorted postfix', () =>{
			const infix = [1,'+',2,'x',3,'+',4];
			const postfix = [1,2,3,'x','+',4,'+'];
			assert.deepEqual(postfix, util.shuntyardSort(infix));
		})
	})
	describe('shuntyardCalc', () =>{
		it('calculate postfix', () =>{
				const sortedPostfix = [1,2,3,'x','+',4,'+'];
				assert.equal(11, util.shuntyardCalc(sortedPostfix));
		})
		it('calculate postfix with float values', () => {
			assert.equal(17.34,util.shuntyardCalc([12.34, 5, "+"]));
		})
		it('calculate postfix with negative numbers', () => {
			assert.equal(-1,util.shuntyardCalc([2,-3,"+"]));
		})
	})

	describe('grabLastToken', () =>{
		it('grab last numeric token', () =>{
			assert.equal("123",util.grabLastToken("99999+123"));
		})
		it('do nothing if arg is empty', () =>{
			assert.equal("",util.grabLastToken(""));
		})
		it('return operator if last char', () =>{
			assert.equal("+",util.grabLastToken("99+"));
		})
		it('handle floats', () =>{
			assert.equal("0.",util.grabLastToken("0."));
		})
	})
}) // END UTIL


// RUN MOCHA
mocha.run()

            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.

Console