Shuntyard Javascript Calculator with unit tests

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
6
down vote

favorite
1












I have been writing a javascript calculator for about a few weeks. It uses a shuntyard algorithm to do order of operations. Some unit tests I have not finished yet and there is some functionality missing (e.g. no display limitations, some display errors) but the core logic behaves as expected.



My goal was to practice functional-programming principles, TDD, and code organization.



The hardest part in writing this was



  • Writing in a clean concise scalable testable manner

  • Which ES6 syntax I could use for conciseness

  • On a MV* Pattern, deciding the functionality logic on the * pattern

  • Determining the functionality of the render method

Function wise I had these issues



  • Debating on what arguments and parameters functions should have

  • Trying to avoid functions with side effects

  • Trying to avoid multiple return paths in a function

  • Deciding how to group similar functions

What I wrote below is pretty sloppy IMO but I need advice on what I can do better



https://codepen.io/Kagerjay/pen/XqNGqv






// 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)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 == ""


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);



var model =
getAnswer: function(cache)
return cache.split('=')[1];
,
pushDot: function(cache, lastCall)
if(lastCall=="calculate" ,
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)-) Seek Operators.
// 2. (?= Conditional check....
// 3. [^(+,
calculate: function(cache, lastCall)
if( isOper.test(cache.slice(-1)) ,
;

// 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);

);
);


// TESTS


// 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()

/*********************** MOCHA TDD STYLES ****************/
.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 */

.display
background-color: #c3c2ab;
/* retro green */
border-radius: 10px;
border: 2px solid black;
text-align: right;
padding-right: 5px;

.display #output
font-size: 20px;

.display #entry
color: grey;

.display 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;

.buttons button
padding: 5px;
border-radius: 5px;
font-size: 110%;
background-color: black;
color: white;

.buttons button[value="AC"], .buttons button[value="CE"]
background-color: #a72d45;
/* dark red */

.buttons #equal-button
grid-column: 0.8;
grid-row: 0.66667;

.buttons #zero-button
grid-row: 0.83333;
grid-column: 0.33333;

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- <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>









share|improve this question



















  • I think I'll just have to read addy's design pattern book, and reverse engineer how other javascript libraries organize their code (e.g. lodash)
    – Vincent Tang
    Apr 28 at 17:49










  • as side note most of the conventions I used here were based on functional programming and things i learned in watchandcode.com
    – Vincent Tang
    Jun 28 at 13:12










  • also i ended up going overboard on unit tests , this was the first program I had started learning TDD /BDD, so I understand now its pitfalls whenever I had to refactor and had to rewrite every test as well
    – Vincent Tang
    Jun 28 at 13:18
















up vote
6
down vote

favorite
1












I have been writing a javascript calculator for about a few weeks. It uses a shuntyard algorithm to do order of operations. Some unit tests I have not finished yet and there is some functionality missing (e.g. no display limitations, some display errors) but the core logic behaves as expected.



My goal was to practice functional-programming principles, TDD, and code organization.



The hardest part in writing this was



  • Writing in a clean concise scalable testable manner

  • Which ES6 syntax I could use for conciseness

  • On a MV* Pattern, deciding the functionality logic on the * pattern

  • Determining the functionality of the render method

Function wise I had these issues



  • Debating on what arguments and parameters functions should have

  • Trying to avoid functions with side effects

  • Trying to avoid multiple return paths in a function

  • Deciding how to group similar functions

What I wrote below is pretty sloppy IMO but I need advice on what I can do better



https://codepen.io/Kagerjay/pen/XqNGqv






// 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)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 == ""


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);



var model =
getAnswer: function(cache)
return cache.split('=')[1];
,
pushDot: function(cache, lastCall)
if(lastCall=="calculate" ,
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)-) Seek Operators.
// 2. (?= Conditional check....
// 3. [^(+,
calculate: function(cache, lastCall)
if( isOper.test(cache.slice(-1)) ,
;

// 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);

);
);


// TESTS


// 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()

/*********************** MOCHA TDD STYLES ****************/
.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 */

.display
background-color: #c3c2ab;
/* retro green */
border-radius: 10px;
border: 2px solid black;
text-align: right;
padding-right: 5px;

.display #output
font-size: 20px;

.display #entry
color: grey;

.display 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;

.buttons button
padding: 5px;
border-radius: 5px;
font-size: 110%;
background-color: black;
color: white;

.buttons button[value="AC"], .buttons button[value="CE"]
background-color: #a72d45;
/* dark red */

.buttons #equal-button
grid-column: 0.8;
grid-row: 0.66667;

.buttons #zero-button
grid-row: 0.83333;
grid-column: 0.33333;

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- <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>









share|improve this question



















  • I think I'll just have to read addy's design pattern book, and reverse engineer how other javascript libraries organize their code (e.g. lodash)
    – Vincent Tang
    Apr 28 at 17:49










  • as side note most of the conventions I used here were based on functional programming and things i learned in watchandcode.com
    – Vincent Tang
    Jun 28 at 13:12










  • also i ended up going overboard on unit tests , this was the first program I had started learning TDD /BDD, so I understand now its pitfalls whenever I had to refactor and had to rewrite every test as well
    – Vincent Tang
    Jun 28 at 13:18












up vote
6
down vote

favorite
1









up vote
6
down vote

favorite
1






1





I have been writing a javascript calculator for about a few weeks. It uses a shuntyard algorithm to do order of operations. Some unit tests I have not finished yet and there is some functionality missing (e.g. no display limitations, some display errors) but the core logic behaves as expected.



My goal was to practice functional-programming principles, TDD, and code organization.



The hardest part in writing this was



  • Writing in a clean concise scalable testable manner

  • Which ES6 syntax I could use for conciseness

  • On a MV* Pattern, deciding the functionality logic on the * pattern

  • Determining the functionality of the render method

Function wise I had these issues



  • Debating on what arguments and parameters functions should have

  • Trying to avoid functions with side effects

  • Trying to avoid multiple return paths in a function

  • Deciding how to group similar functions

What I wrote below is pretty sloppy IMO but I need advice on what I can do better



https://codepen.io/Kagerjay/pen/XqNGqv






// 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)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 == ""


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);



var model =
getAnswer: function(cache)
return cache.split('=')[1];
,
pushDot: function(cache, lastCall)
if(lastCall=="calculate" ,
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)-) Seek Operators.
// 2. (?= Conditional check....
// 3. [^(+,
calculate: function(cache, lastCall)
if( isOper.test(cache.slice(-1)) ,
;

// 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);

);
);


// TESTS


// 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()

/*********************** MOCHA TDD STYLES ****************/
.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 */

.display
background-color: #c3c2ab;
/* retro green */
border-radius: 10px;
border: 2px solid black;
text-align: right;
padding-right: 5px;

.display #output
font-size: 20px;

.display #entry
color: grey;

.display 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;

.buttons button
padding: 5px;
border-radius: 5px;
font-size: 110%;
background-color: black;
color: white;

.buttons button[value="AC"], .buttons button[value="CE"]
background-color: #a72d45;
/* dark red */

.buttons #equal-button
grid-column: 0.8;
grid-row: 0.66667;

.buttons #zero-button
grid-row: 0.83333;
grid-column: 0.33333;

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- <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>









share|improve this question











I have been writing a javascript calculator for about a few weeks. It uses a shuntyard algorithm to do order of operations. Some unit tests I have not finished yet and there is some functionality missing (e.g. no display limitations, some display errors) but the core logic behaves as expected.



My goal was to practice functional-programming principles, TDD, and code organization.



The hardest part in writing this was



  • Writing in a clean concise scalable testable manner

  • Which ES6 syntax I could use for conciseness

  • On a MV* Pattern, deciding the functionality logic on the * pattern

  • Determining the functionality of the render method

Function wise I had these issues



  • Debating on what arguments and parameters functions should have

  • Trying to avoid functions with side effects

  • Trying to avoid multiple return paths in a function

  • Deciding how to group similar functions

What I wrote below is pretty sloppy IMO but I need advice on what I can do better



https://codepen.io/Kagerjay/pen/XqNGqv






// 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)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 == ""


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);



var model =
getAnswer: function(cache)
return cache.split('=')[1];
,
pushDot: function(cache, lastCall)
if(lastCall=="calculate" ,
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)-) Seek Operators.
// 2. (?= Conditional check....
// 3. [^(+,
calculate: function(cache, lastCall)
if( isOper.test(cache.slice(-1)) ,
;

// 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);

);
);


// TESTS


// 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()

/*********************** MOCHA TDD STYLES ****************/
.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 */

.display
background-color: #c3c2ab;
/* retro green */
border-radius: 10px;
border: 2px solid black;
text-align: right;
padding-right: 5px;

.display #output
font-size: 20px;

.display #entry
color: grey;

.display 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;

.buttons button
padding: 5px;
border-radius: 5px;
font-size: 110%;
background-color: black;
color: white;

.buttons button[value="AC"], .buttons button[value="CE"]
background-color: #a72d45;
/* dark red */

.buttons #equal-button
grid-column: 0.8;
grid-row: 0.66667;

.buttons #zero-button
grid-row: 0.83333;
grid-column: 0.33333;

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- <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>








// 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)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 == ""


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);



var model =
getAnswer: function(cache)
return cache.split('=')[1];
,
pushDot: function(cache, lastCall)
if(lastCall=="calculate" ,
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)-) Seek Operators.
// 2. (?= Conditional check....
// 3. [^(+,
calculate: function(cache, lastCall)
if( isOper.test(cache.slice(-1)) ,
;

// 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);

);
);


// TESTS


// 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()

/*********************** MOCHA TDD STYLES ****************/
.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 */

.display
background-color: #c3c2ab;
/* retro green */
border-radius: 10px;
border: 2px solid black;
text-align: right;
padding-right: 5px;

.display #output
font-size: 20px;

.display #entry
color: grey;

.display 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;

.buttons button
padding: 5px;
border-radius: 5px;
font-size: 110%;
background-color: black;
color: white;

.buttons button[value="AC"], .buttons button[value="CE"]
background-color: #a72d45;
/* dark red */

.buttons #equal-button
grid-column: 0.8;
grid-row: 0.66667;

.buttons #zero-button
grid-row: 0.83333;
grid-column: 0.33333;

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- <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>





// 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)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 == ""


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);



var model =
getAnswer: function(cache)
return cache.split('=')[1];
,
pushDot: function(cache, lastCall)
if(lastCall=="calculate" ,
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)-) Seek Operators.
// 2. (?= Conditional check....
// 3. [^(+,
calculate: function(cache, lastCall)
if( isOper.test(cache.slice(-1)) ,
;

// 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);

);
);


// TESTS


// 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()

/*********************** MOCHA TDD STYLES ****************/
.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 */

.display
background-color: #c3c2ab;
/* retro green */
border-radius: 10px;
border: 2px solid black;
text-align: right;
padding-right: 5px;

.display #output
font-size: 20px;

.display #entry
color: grey;

.display 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;

.buttons button
padding: 5px;
border-radius: 5px;
font-size: 110%;
background-color: black;
color: white;

.buttons button[value="AC"], .buttons button[value="CE"]
background-color: #a72d45;
/* dark red */

.buttons #equal-button
grid-column: 0.8;
grid-row: 0.66667;

.buttons #zero-button
grid-row: 0.83333;
grid-column: 0.33333;

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- <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>








share|improve this question










share|improve this question




share|improve this question









asked Apr 28 at 3:49









Vincent Tang

1334




1334











  • I think I'll just have to read addy's design pattern book, and reverse engineer how other javascript libraries organize their code (e.g. lodash)
    – Vincent Tang
    Apr 28 at 17:49










  • as side note most of the conventions I used here were based on functional programming and things i learned in watchandcode.com
    – Vincent Tang
    Jun 28 at 13:12










  • also i ended up going overboard on unit tests , this was the first program I had started learning TDD /BDD, so I understand now its pitfalls whenever I had to refactor and had to rewrite every test as well
    – Vincent Tang
    Jun 28 at 13:18
















  • I think I'll just have to read addy's design pattern book, and reverse engineer how other javascript libraries organize their code (e.g. lodash)
    – Vincent Tang
    Apr 28 at 17:49










  • as side note most of the conventions I used here were based on functional programming and things i learned in watchandcode.com
    – Vincent Tang
    Jun 28 at 13:12










  • also i ended up going overboard on unit tests , this was the first program I had started learning TDD /BDD, so I understand now its pitfalls whenever I had to refactor and had to rewrite every test as well
    – Vincent Tang
    Jun 28 at 13:18















I think I'll just have to read addy's design pattern book, and reverse engineer how other javascript libraries organize their code (e.g. lodash)
– Vincent Tang
Apr 28 at 17:49




I think I'll just have to read addy's design pattern book, and reverse engineer how other javascript libraries organize their code (e.g. lodash)
– Vincent Tang
Apr 28 at 17:49












as side note most of the conventions I used here were based on functional programming and things i learned in watchandcode.com
– Vincent Tang
Jun 28 at 13:12




as side note most of the conventions I used here were based on functional programming and things i learned in watchandcode.com
– Vincent Tang
Jun 28 at 13:12












also i ended up going overboard on unit tests , this was the first program I had started learning TDD /BDD, so I understand now its pitfalls whenever I had to refactor and had to rewrite every test as well
– Vincent Tang
Jun 28 at 13:18




also i ended up going overboard on unit tests , this was the first program I had started learning TDD /BDD, so I understand now its pitfalls whenever I had to refactor and had to rewrite every test as well
– Vincent Tang
Jun 28 at 13:18










1 Answer
1






active

oldest

votes

















up vote
3
down vote



accepted










I would maybe suggest that the tests currently focus on the "happy path" and some other test might help highlight a few other bugs/features.



e.g. after performing calculate if another calculate operation is performed the previous expression is lost. Not sure if thats a bug or feature, but i think shows the kind of thing i'm talking about.



i would also be tempted to join the isOper and operations somehow so that its a "single" change in order to add a new operation, maybe something like...



var operations = 
'x': apply: function(a,b) return b*a, match: /x/ ,
'÷': apply: function(a,b) return b/a, match: /÷/ ,
'+': apply: function(a,b) return b+a, match: /+/ ,
'-': apply: function(a,b) return b-a, match: /-/ ,






share|improve this answer





















  • I didn't have calculate remember anything, I purposely made it that way to have a more "functional" programming style, e.g. everything got passed to functions only. In hindsight, writing this calculator made me realize that writing logic this way is unnatural since the natural flow of things tend to generally follow a more OOP approach. E.g., actual calculators write things to memory, etc. thanks for the input though. I didn't think about joining isOper to add a new operation. I'm going to later rewrite the calculator in React framework though
    – Vincent Tang
    Jun 26 at 20:27










Your Answer




StackExchange.ifUsing("editor", function ()
return StackExchange.using("mathjaxEditing", function ()
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
);
);
, "mathjax-editing");

StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");

StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);

else
createEditor();

);

function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: false,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);



);








 

draft saved


draft discarded


















StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f193128%2fshuntyard-javascript-calculator-with-unit-tests%23new-answer', 'question_page');

);

Post as a guest






























1 Answer
1






active

oldest

votes








1 Answer
1






active

oldest

votes









active

oldest

votes






active

oldest

votes








up vote
3
down vote



accepted










I would maybe suggest that the tests currently focus on the "happy path" and some other test might help highlight a few other bugs/features.



e.g. after performing calculate if another calculate operation is performed the previous expression is lost. Not sure if thats a bug or feature, but i think shows the kind of thing i'm talking about.



i would also be tempted to join the isOper and operations somehow so that its a "single" change in order to add a new operation, maybe something like...



var operations = 
'x': apply: function(a,b) return b*a, match: /x/ ,
'÷': apply: function(a,b) return b/a, match: /÷/ ,
'+': apply: function(a,b) return b+a, match: /+/ ,
'-': apply: function(a,b) return b-a, match: /-/ ,






share|improve this answer





















  • I didn't have calculate remember anything, I purposely made it that way to have a more "functional" programming style, e.g. everything got passed to functions only. In hindsight, writing this calculator made me realize that writing logic this way is unnatural since the natural flow of things tend to generally follow a more OOP approach. E.g., actual calculators write things to memory, etc. thanks for the input though. I didn't think about joining isOper to add a new operation. I'm going to later rewrite the calculator in React framework though
    – Vincent Tang
    Jun 26 at 20:27














up vote
3
down vote



accepted










I would maybe suggest that the tests currently focus on the "happy path" and some other test might help highlight a few other bugs/features.



e.g. after performing calculate if another calculate operation is performed the previous expression is lost. Not sure if thats a bug or feature, but i think shows the kind of thing i'm talking about.



i would also be tempted to join the isOper and operations somehow so that its a "single" change in order to add a new operation, maybe something like...



var operations = 
'x': apply: function(a,b) return b*a, match: /x/ ,
'÷': apply: function(a,b) return b/a, match: /÷/ ,
'+': apply: function(a,b) return b+a, match: /+/ ,
'-': apply: function(a,b) return b-a, match: /-/ ,






share|improve this answer





















  • I didn't have calculate remember anything, I purposely made it that way to have a more "functional" programming style, e.g. everything got passed to functions only. In hindsight, writing this calculator made me realize that writing logic this way is unnatural since the natural flow of things tend to generally follow a more OOP approach. E.g., actual calculators write things to memory, etc. thanks for the input though. I didn't think about joining isOper to add a new operation. I'm going to later rewrite the calculator in React framework though
    – Vincent Tang
    Jun 26 at 20:27












up vote
3
down vote



accepted







up vote
3
down vote



accepted






I would maybe suggest that the tests currently focus on the "happy path" and some other test might help highlight a few other bugs/features.



e.g. after performing calculate if another calculate operation is performed the previous expression is lost. Not sure if thats a bug or feature, but i think shows the kind of thing i'm talking about.



i would also be tempted to join the isOper and operations somehow so that its a "single" change in order to add a new operation, maybe something like...



var operations = 
'x': apply: function(a,b) return b*a, match: /x/ ,
'÷': apply: function(a,b) return b/a, match: /÷/ ,
'+': apply: function(a,b) return b+a, match: /+/ ,
'-': apply: function(a,b) return b-a, match: /-/ ,






share|improve this answer













I would maybe suggest that the tests currently focus on the "happy path" and some other test might help highlight a few other bugs/features.



e.g. after performing calculate if another calculate operation is performed the previous expression is lost. Not sure if thats a bug or feature, but i think shows the kind of thing i'm talking about.



i would also be tempted to join the isOper and operations somehow so that its a "single" change in order to add a new operation, maybe something like...



var operations = 
'x': apply: function(a,b) return b*a, match: /x/ ,
'÷': apply: function(a,b) return b/a, match: /÷/ ,
'+': apply: function(a,b) return b+a, match: /+/ ,
'-': apply: function(a,b) return b-a, match: /-/ ,







share|improve this answer













share|improve this answer



share|improve this answer











answered Jun 26 at 8:21









Chris Matheson

462




462











  • I didn't have calculate remember anything, I purposely made it that way to have a more "functional" programming style, e.g. everything got passed to functions only. In hindsight, writing this calculator made me realize that writing logic this way is unnatural since the natural flow of things tend to generally follow a more OOP approach. E.g., actual calculators write things to memory, etc. thanks for the input though. I didn't think about joining isOper to add a new operation. I'm going to later rewrite the calculator in React framework though
    – Vincent Tang
    Jun 26 at 20:27
















  • I didn't have calculate remember anything, I purposely made it that way to have a more "functional" programming style, e.g. everything got passed to functions only. In hindsight, writing this calculator made me realize that writing logic this way is unnatural since the natural flow of things tend to generally follow a more OOP approach. E.g., actual calculators write things to memory, etc. thanks for the input though. I didn't think about joining isOper to add a new operation. I'm going to later rewrite the calculator in React framework though
    – Vincent Tang
    Jun 26 at 20:27















I didn't have calculate remember anything, I purposely made it that way to have a more "functional" programming style, e.g. everything got passed to functions only. In hindsight, writing this calculator made me realize that writing logic this way is unnatural since the natural flow of things tend to generally follow a more OOP approach. E.g., actual calculators write things to memory, etc. thanks for the input though. I didn't think about joining isOper to add a new operation. I'm going to later rewrite the calculator in React framework though
– Vincent Tang
Jun 26 at 20:27




I didn't have calculate remember anything, I purposely made it that way to have a more "functional" programming style, e.g. everything got passed to functions only. In hindsight, writing this calculator made me realize that writing logic this way is unnatural since the natural flow of things tend to generally follow a more OOP approach. E.g., actual calculators write things to memory, etc. thanks for the input though. I didn't think about joining isOper to add a new operation. I'm going to later rewrite the calculator in React framework though
– Vincent Tang
Jun 26 at 20:27












 

draft saved


draft discarded


























 


draft saved


draft discarded














StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f193128%2fshuntyard-javascript-calculator-with-unit-tests%23new-answer', 'question_page');

);

Post as a guest













































































Popular posts from this blog

Chat program with C++ and SFML

Function to Return a JSON Like Objects Using VBA Collections and Arrays

Will my employers contract hold up in court?