├── 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 |
14 |
15 |
16 |
AC
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
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 | --------------------------------------------------------------------------------