Calculator Snippet CC

The April 2015 Community Challenge requires building a calculator.

Implement a simple calculator

Where the definition of "simple" is whatever you make it - only supports basic arithmetic operators? Fine. It supports scientific notation, exponents and trigonometry? Fine! Takes input from the console? Fine! Toggle between binary, hex, octal and decimal notations? Fine!

The common denominator here, is that you need:

• A way to take user input
• A way to parse/validate user input
• A way to perform the operations in the input
• A way to output the result(s)

Everything else you want to put in, is up to you and the time you can devote to this small project in the limited number of days we have until April is over - be creative!

I am specifically looking for insights on the way I have integrated with the keypressed concept, the way that the system can be extended by adding binary or unary operators, and for any mathematical oversights I currently may have.

I am learning Javascript, CSS, and d3 and I am somewhat sure I am missing basic practices that I should have too. If you see anything that's contrary to industry best practices, please point those out too.

Note: I found that the snippet does odd things with key-presses occasionally: here is the standalone 'original' version that I 'hacked' to make in to a snippet: standalone calc.html. I find the experience of the standalone version is better.

var calc = {
stack: {
values: new Array(1024),
size: 0,
decimal: 0
},

display: {
value: 0.0,
integral: true
},

layout: [
{
compact: false,
buttons: ["seven", "eight", "nine", "root", "clear"]
},
{
compact: false,
buttons: ["four", "five", "six", "times", "divide"]
},
{
compact: false,
buttons: ["one", "two", "three", "plus", "minus"]
},
{
compact: false,
buttons: ["zero", "point", "plusminus", "exp", "equals"]
},

],

keys: {},

buttons: {
zero: {
action: function() {return digit(0);},
label: "0",
key: [48]
},
one: {
action: function() {return digit(1);},
label: "1",
key: [49]
},
two: {
action: function() {return digit(2);},
label: "2",
key: [50]
},
three: {
action: function() {return digit(3);},
label: "3",
key: [51]
},
four: {
action: function() {return digit(4);},
label: "4",
key: [52]
},
five: {
action: function() {return digit(5);},
label: "5",
key: [53]
},
six: {
action: function() {return digit(6);},
label: "6",
key: [54]
},
seven: {
action: function() {return digit(7);},
label: "7",
key: [55]
},
eight: {
action: function() {return digit(8);},
label: "8",
key: [56]
},
nine: {
action: function() {return digit(9);},
label: "9",
key: [57]
},

point: {
action: function() {
if (calc.stack.decimal === 0) {
calc.stack.decimal = 1;
}
return peek();
},
label: ".",
key: [46]
},

root: {
action: function() {
return unaryOp(function(val) {
return Math.sqrt(val);
});
},
label: "r\u221A",
color: "black",
key: [114]
},

plusminus: {
action: function() {
return unaryOp(function(val) {
return -val;
});
},
label: "~\u00B1",
key: [126]
},

plus: {
action: function() {
return binaryOp(calc.buttons.plus);
},
operate: function(left, right) {
return left + right;
},
label: "+",
color: "black",
key: [43]
},
minus: {
action: function() {
return binaryOp(calc.buttons.minus);
},
operate: function(left, right) {
return left - right;
},
label: "-",
color: "black",
key: [45]
},
times: {
action: function() {
return binaryOp(calc.buttons.times);
},
operate: function(left, right) {
return left * right;
},
label: "*",
color: "black",
key: [42,120]
},
divide: {
action: function() {
return binaryOp(calc.buttons.divide);
},
operate: function(left, right) {
return left / right;
},
label: "/\u00F7",
color: "black",
key: [47]
},
exp: {
action: function() {
return binaryOp(calc.buttons.exp);
},
operate: function(left, right) {
return Math.pow(left , right);
},
label: "EXP^",
color: "black",
key: [94]
},
equals: {
action: function() {resolve();},
label: "=",
color: "black",
key: [61,13]
},
clear: {
action: function() {reset();},
label: "del",
color: "orange",
key: [32, 127]
}
}

};

function peek() {
if (calc.stack.size == 0) {
throw "Empty stack peek()";
}
return calc.stack.values[calc.stack.size - 1];
}

function pop() {
if (calc.stack.size == 0) {
throw "Empty stack pop()";
}
calc.stack.size--;
var val = calc.stack.values[calc.stack.size];
calc.stack.values[calc.stack.size] = null;
return val;
}

function push(val) {
calc.stack.values[calc.stack.size++] = val;
return val;
}

function cleared() {
return calc.stack.size == 0;
}

function reset() {
while(calc.stack.size != 0) {
pop();
}
push(0);
calc.stack.decimal = 0;
}

function updateDisplay() {
var disp = "";
for (var i = 0; i < calc.stack.size; i++) {
if (typeof calc.stack.values[i] == "number") {
disp += " " + calc.stack.values[i];
} else {
disp += " " + calc.stack.values[i].label;
}
}
d3.select("#display")
.attr("value", "" + disp);
}

function resolve() {
var val = pop();
while (!cleared()) {
var op = pop();
var left = pop();
var eq = op.operate(left,val);
console.log("Compute: " + left + " " + op.label + " " + val + " -> " + eq);
val = eq;
}
push(val);
calc.stack.decimal = 0;
}

function unaryOp(fn) {
// unary expects a value at stack-top.
if ("number" != typeof peek()) {
console.log("Evict " + pop());
}
push(fn(pop()));
}

function binaryOp(fn) {
resolve();
push(fn);
push(0);
}

function digit(val) {
if (calc.stack.decimal === 0) {
return push(pop() * 10 + val);
}
calc.stack.decimal /= 10.0;
var current = pop();
var n = calc.stack.decimal * val;
var tp = n + current;
//console.log("Current " + current + " decimal " + calc.stack.decimal + "... new " + n + " to push " + tp);
return push(tp);
}

function pressButton(name) {
var detail = calc.buttons[name];
//console.log("Button pressed: " + name);
detail.action();
d3.select("#" + name)
.style("background-color", "red")
.transition()
.style("background-color", detail.color);
updateDisplay();
}

function keyHook(kevent) {
var k = kevent.keyCode || kevent.which;
if (calc.keys[k]) {
console.log("Keypress code " + kevent.keyCode + " which: " + kevent.which + " linked to " + calc.keys[k]);
pressButton(calc.keys[k]);
return;
}
console.log("Unrecognized Keypress " + k);
}

function setup() {

var buttons = d3.select("#buttons");

calc.layout.forEach(function(row) {
row.buttons.forEach(function(bid){
var b = calc.buttons[bid];
b.id = bid;
b.color = b.color || "darkslategray";
var title = "Key:";
b.key.forEach(function(k){
var kname = String.fromCharCode(k);
title = title + " '" + kname + "'";
calc.keys[k] = bid;
});
buttons.append("input")
.classed("button",true)
.style("background-color", b.color)
.attr("title", title)
.attr("type", "button")
.attr("id", bid)
.attr("value", b.label)
.attr("onclick", "pressButton(this.id);");
});
buttons.append("br");
});
window.onkeypress=keyHook;
reset();
updateDisplay();
}

setup();
    #display {
text-align: right;
font: bold 50px monospace;
background-color: darkgray;
width: 100%;
box-sizing: border-box;
}

.button {
font: bold 35px monospace;
color: white;
margin: 10px;
width: 3em;
}

table {
border-collapse: collapse;
margin: 0 auto
}

table.main {
background-color: lightgray;
border: 2px solid darkgray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

<table class="main">
<caption>CodeReview Calculator</caption>
<tr>
<td>
<input id="display" value="0" size="15" readonly="true" >
</td>
</tr>
<tr>
<td>
<div id="buttons"></div>
</td>
</tr>

</table>

• Wow, that's pretty cool! – Mathieu Guindon Apr 11 '15 at 0:59
• I wrote one too. Yours does more than mine, though. Feel free to swipe ideas -- CSS, drag and drop, fonts, whatever. You've got a bug, though. Negative results don't appear negative. Try doing 5 - 10. – rojo Apr 11 '15 at 19:24

Beyond not having the basic framework for the HTML, you have one error according to the W3C validator:

<input id="display" value="0" size="15" readonly="true" >


readonly="true" is not valid, the correct attribute is readonly="readonly"

There is a pretty bad UI problem here. When you click the buttons as follows 9 - 3 = 2, the calculator reads 62, whereas it should reset and only read 2 because you clicked the = button.

Beyond this, your calculator does not follow the order of operations. Probably the simplest way to handle this would be to only update the calculation when the +, -, and = buttons are pressed (like the Windows calculator does), parsing and calculating everything else from the stack.

• I've always read that the correct way to specify readonly is to simply include readonly without a value in the element. – Grant Miller Jun 19 '18 at 1:27
• That probably works in some browsers, but not necessarily in all. The same applies for selected on an option element in a select. Normally, you'd just do selected, but sometimes, you need to do selected="selected". Also disabled. – Hosch250 Jun 19 '18 at 1:33

I see a lot of repeated code, especially in the construction of the buttons, double especially for the digit buttons. It seems that they should be constructible by a for loop and an appropriate data structure.