├── style.css
├── index.html
└── calc.js
/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100vh;
4 | padding: 0;
5 | margin: 0;
6 | font-family: monospace;
7 | }
8 |
9 | body {
10 | display: flex;
11 | flex-direction: column;
12 | align-items: center;
13 | justify-content: space-between;
14 | background: #0e1111;
15 | }
16 |
17 | .calculator {
18 | width: 250px;
19 | padding: 30% 10%;
20 | }
21 |
22 | .display {
23 | background-color: #0e1111;
24 | color: #149414;
25 | font-size: xx-large;
26 | font-weight: bold;
27 | text-align: right;
28 | padding-right: 10px;
29 | }
30 |
31 | .grid {
32 | display: flex;
33 | flex-direction: column;
34 | }
35 |
36 | .grid > .row {
37 | flex: 1;
38 | display: flex;
39 | }
40 |
41 | .button {
42 | background-color: dimgray;
43 | flex: 1;
44 | width: 10%;
45 | font-size: x-large;
46 | border-radius: 10%;
47 | margin: 2px;
48 | padding: 8px;
49 | user-select: none;
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | }
54 |
55 | .button.special {
56 | background-color: #149414;
57 | color: #0e1111;
58 | }
59 |
60 | .button.op,
61 | .button.eval {
62 | background-color: #0e6b0e;
63 | color: #0e1111;
64 | }
65 |
66 | .button.double {
67 | flex: 2;
68 | }
69 |
70 | #foot {
71 | background: #0e1111;
72 | flex-shrink: 0;
73 | height: 18px;
74 | width: 100%;
75 | display: flex;
76 | flex-direction: column;
77 | align-items: center;
78 | justify-content: center;
79 | font-size: 12px;
80 | font-weight: 100;
81 | color: #414a4c;
82 | }
83 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
24 |
30 |
36 |
41 |
42 |
43 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/calc.js:
--------------------------------------------------------------------------------
1 | const MAXWIDTH = 16;
2 |
3 | // the calculator never has more than one binary op in flight
4 | // at any given time,
5 | // so we structure the program around a finite automaton, with four STATEs:
6 | // - AWAITing numbers or an operator while displaying the LEFT-hand-side
7 | // - ENTERing numbers to edit the LEFT-hand-side of the expression
8 | // - AWAITing numbers or an operator while displaying the RIGHT-hand-side
9 | // - ENTERing numbers to the the RIGHT-hand-side of the expression
10 | // key presses can cause a change to that STATE and/or to the values of the
11 | // LHS and RHS of the expression and the OPerator between them.
12 |
13 | let STATE = "AWAIT-LEFT"; // what state is the calculator in?
14 | let LHS = "0"; // what's the left-hand-side of the current expression?
15 | let OP = ""; // what's the operator in the current expression?
16 | let RHS = ""; // what the right-hand-side of the current expression?
17 |
18 | const evaluate = function() {
19 | const result = operate(OP, LHS, RHS);
20 | LHS = result;
21 | OP = "";
22 | RHS = "";
23 | STATE = "AWAIT-LEFT";
24 | return result;
25 | };
26 |
27 | const reset = function() {
28 | STATE = "AWAIT-LEFT";
29 | LHS = "0";
30 | OP = "";
31 | RHS = "";
32 | };
33 |
34 | const runKeypress = function(key) {
35 | const keyClass = getKeyClass(key);
36 | if (keyClass === "clear") {
37 | reset();
38 | } else {
39 | switch (STATE) {
40 | case "AWAIT-LEFT":
41 | runKeypressAwaitLeft(key, keyClass);
42 | break;
43 | case "ENTER-LEFT":
44 | runKeypressEnterLeft(key, keyClass);
45 | break;
46 | case "ENTER-RIGHT":
47 | runKeypressEnterRight(key, keyClass);
48 | break;
49 | case "AWAIT-RIGHT":
50 | runKeypressAwaitRight(key, keyClass);
51 | break;
52 | default:
53 | throw new Error(`bad state ${STATE}`);
54 | };
55 | };
56 | updateDisplay();
57 | };
58 |
59 | const updateDisplay = function() {
60 | console.log(`${STATE}: (${LHS} ${OP} ${RHS})`)
61 | let displayContent;
62 | switch (STATE) {
63 | case "AWAIT-LEFT":
64 | case "ENTER-LEFT":
65 | case "AWAIT-RIGHT":
66 | displayContent = LHS;
67 | break;
68 | case "ENTER-RIGHT":
69 | displayContent = RHS;
70 | break;
71 | default:
72 | throw new Error(`bad state ${STATE}`);
73 | }
74 |
75 | DISPLAY.innerText = displayContent;
76 | console.log(displayContent)
77 | };
78 |
79 | //
80 | // Core logic is in these four runKeypress functions,
81 | // one for each STATE of the machine
82 | //
83 |
84 | const runKeypressAwaitLeft = function(key, keyClass) {
85 | switch (keyClass) {
86 | case "edit":
87 | STATE = "ENTER-LEFT";
88 | LHS = updateString("", key);
89 | break;
90 | case "eval":
91 | break;
92 | case "op":
93 | OP = opFrom(key);
94 | STATE = "AWAIT-RIGHT";
95 | break;
96 | default:
97 | throw new Error(`bad keyclass ${keyClass} in ${STATE}`);
98 | };
99 | };
100 |
101 | const runKeypressAwaitRight = function(key, keyClass) {
102 | switch (keyClass) {
103 | case "edit":
104 | STATE = "ENTER-RIGHT";
105 | RHS = updateString("", key);
106 | break;
107 | case "eval":
108 | break;
109 | case "op":
110 | OP = opFrom(key);
111 | RHS = LHS;
112 | evaluate();
113 | break;
114 | default:
115 | throw new Error(`bad keyclass ${keyClass} in ${STATE}`);
116 | };
117 | };
118 |
119 | const runKeypressEnterLeft = function(key, keyClass) {
120 | switch (keyClass) {
121 | case "edit":
122 | LHS = updateString(LHS, key);
123 | break;
124 | case "eval":
125 | break;
126 | case "op":
127 | OP = opFrom(key);
128 | STATE = "AWAIT-RIGHT";
129 | break;
130 | default:
131 | throw new Error(`bad keyclass ${keyClass} in ${STATE}`);
132 | };
133 | };
134 |
135 | const runKeypressEnterRight = function(key, keyClass) {
136 | switch (keyClass) {
137 | case "edit":
138 | RHS = updateString(RHS, key);
139 | break;
140 | case "eval":
141 | evaluate();
142 | STATE = "AWAIT-LEFT";
143 | break;
144 | case "op":
145 | evaluate();
146 | OP = opFrom(key);
147 | STATE = "AWAIT-RIGHT";
148 | break;
149 | default:
150 | throw new Error(`bad keyclass ${keyClass} in ${STATE}`);
151 | };
152 | };
153 |
154 | const getKeyClass = function(key) {
155 | if (key === "clear") {
156 | return "clear";
157 | };
158 | if (EDITS.includes(key)) {
159 | return "edit";
160 | };
161 | if (OPS.includes(key)) {
162 | return "op";
163 | };
164 | if (EVALS.includes(key)) {
165 | return "eval";
166 | };
167 | throw new Error(`class of key ${key} not understood`);
168 | };
169 |
170 | //
171 | // These functions handle logic for ops
172 | //
173 |
174 | const opFrom = function(str) {
175 | if (str.length > 1) {
176 | throw new Error(`bad op string ${str}`);
177 | } else if (!OPS.includes(str)) {
178 | throw new Error(`bad op string ${str}`);
179 | };
180 | return str;
181 | };
182 |
183 | const add = function(lhs, rhs) {
184 | return +lhs + +rhs;
185 | };
186 |
187 | const subtract = function(lhs, rhs) {
188 | return +lhs - +rhs;
189 | };
190 |
191 | const multiply = function(lhs, rhs) {
192 | return +lhs * +rhs;
193 | };
194 |
195 | const divide = function(lhs, rhs) {
196 | return +lhs / +rhs;
197 | };
198 |
199 | const operate = function(op, lhs, rhs) {
200 | switch (op) {
201 | case "+":
202 | return toString(add(lhs, rhs));
203 | case "-":
204 | return toString(subtract(lhs, rhs));
205 | case "*":
206 | return toString(multiply(lhs, rhs));
207 | case "/":
208 | return toString(divide(lhs, rhs));
209 | default:
210 | throw new Error(`did not understand op ${op}`);
211 | };
212 | };
213 |
214 | //
215 | // updateString, toString, and handleOverflow
216 | // connect the Number op output to Strings for display
217 | //
218 |
219 | const updateString = function(current, k) {
220 | let out;
221 |
222 | if (current === "NaN") {
223 | return "NaN";
224 | };
225 |
226 | if (k === "±") {
227 | if (current.includes("-")) {
228 | if (!current[0] === "-") {
229 | throw new Error(`bad plus-minus in ${current}`)
230 | };
231 | out = current.substring(1)
232 | } else {
233 | out = "-" + current;
234 | };
235 | }
236 |
237 | if (k === ".") {
238 | if (current.includes(".")) {
239 | out = current;
240 | } else {
241 | out = current + ".";
242 | };
243 | };
244 |
245 | if (DIGITS.includes(k)) {
246 | out = current + k;
247 | };
248 |
249 | if (out.length > MAXWIDTH) {
250 | return current;
251 | } else {
252 | return out;
253 | };
254 | };
255 |
256 | const toString = function(num) {
257 | let out = num.toString();
258 | if (out.includes("Infinity")) {
259 | out = "NaN";
260 | };
261 | if (out.length >= MAXWIDTH) {
262 | out = handleOverflow(out)
263 | };
264 | return out;
265 | };
266 |
267 | const handleOverflow = function(stringNum) {
268 | if (stringNum.includes(".")) {
269 | return stringNum.substring(0, MAXWIDTH);
270 | } else {
271 | return "NaN";
272 | };
273 | };
274 |
275 | //
276 | // Connect to DOM for button definition
277 | //
278 |
279 | const DISPLAY = document.querySelector(".display");
280 | const buttons = document.querySelectorAll(".button");
281 |
282 | const digitButtons = Array.from(document.querySelectorAll(".button.digit"));
283 | const editButtons = Array.from(document.querySelectorAll(".button.edit"));
284 | const opButtons = Array.from(document.querySelectorAll(".button.op"));
285 | const evalButtons = Array.from(document.querySelectorAll(".button.eval"));
286 |
287 | const getID = element => element.id;
288 | const concat = (strA, strB) => strA + strB;
289 |
290 | const DIGITS = digitButtons.map(getID).reduce(concat);
291 | const EDITS = editButtons.map(getID).reduce(concat);
292 | const OPS = opButtons.map(getID).reduce(concat);
293 | const EVALS = evalButtons.map(getID).reduce(concat);
294 |
295 | const fillButton = function(button) {
296 | if (button.innerText === "") {
297 | button.innerText = button.id;
298 | };
299 | };
300 |
301 | const attachKeyPress = function(button) {
302 | button.addEventListener("click", (e) => runKeypress(e.target.id) );
303 | };
304 |
305 | buttons.forEach(button => fillButton(button));
306 |
307 | buttons.forEach(button => attachKeyPress(button));
308 |
309 | runKeypress("=");
310 |
--------------------------------------------------------------------------------