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>
5 - 10
. \$\endgroup\$