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>
2 Answers 2
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.
-
\$\begingroup\$ I've always read that the correct way to specify
readonly
is to simply includereadonly
without a value in the element. \$\endgroup\$Grant Miller– Grant Miller2018年06月19日 01:27:58 +00:00Commented Jun 19, 2018 at 1:27 -
\$\begingroup\$ That probably works in some browsers, but not necessarily in all. The same applies for
selected
on anoption
element in aselect
. Normally, you'd just doselected
, but sometimes, you need to doselected="selected"
. Alsodisabled
. \$\endgroup\$user34073– user340732018年06月19日 01:33:23 +00:00Commented Jun 19, 2018 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.
Explore related questions
See similar questions with these tags.
5 - 10
. \$\endgroup\$