Shuntyard Javascript Calculator with unit tests
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
6
down vote
favorite
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>
javascript algorithm unit-testing calculator
add a comment |Â
up vote
6
down vote
favorite
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>
javascript algorithm unit-testing calculator
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
add a comment |Â
up vote
6
down vote
favorite
up vote
6
down vote
favorite
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>
javascript algorithm unit-testing calculator
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>
javascript algorithm unit-testing calculator
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
add a comment |Â
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
add a comment |Â
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: /-/ ,
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 joiningisOper
to add a new operation. I'm going to later rewrite the calculator in React framework though
â Vincent Tang
Jun 26 at 20:27
add a comment |Â
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: /-/ ,
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 joiningisOper
to add a new operation. I'm going to later rewrite the calculator in React framework though
â Vincent Tang
Jun 26 at 20:27
add a comment |Â
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: /-/ ,
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 joiningisOper
to add a new operation. I'm going to later rewrite the calculator in React framework though
â Vincent Tang
Jun 26 at 20:27
add a comment |Â
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: /-/ ,
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: /-/ ,
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 joiningisOper
to add a new operation. I'm going to later rewrite the calculator in React framework though
â Vincent Tang
Jun 26 at 20:27
add a comment |Â
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 joiningisOper
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
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
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
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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