Loading calc.html +56 −6 Original line number Diff line number Diff line Loading @@ -54,6 +54,10 @@ border: 1px grey solid; width: 100%; text-align: right; font-size: 1.5em; } #error-msg { height: 30px; } </style> </head> Loading @@ -69,7 +73,10 @@ <div class="keyboard"> <table> <tr> <td colspan="4" data-bt-name="number-display" id="number-display"></td> <td colspan="4" id="error-msg"></td> </tr> <tr> <td colspan="4" id="number-display"></td> </tr> <tr> <td data-bt-name="num7">7</td> Loading Loading @@ -216,6 +223,22 @@ console.assert(m.numberStack[0] === -9); console.assert(m.numberStack[1] === 29 * 2); }; const rpnBackspaceOperator = function () { const m = new fsm.RPNCalculator(); m.sendKey("num1"); m.sendKey("num2"); console.assert(m.currentNumber === "12"); m.sendKey("backspace"); // remove last digit in current number. console.assert(m.currentNumber === "1"); m.sendKey("num3"); m.sendKey("return"); console.assert(m.currentNumber === ""); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === 13); m.sendKey("backspace"); // delete last number in stack. console.assert(m.numberStack.length === 0); }; const rpnTestNumberErrorHandling = function () { var m = new fsm.RPNCalculator(); m.sendKey("num0"); Loading Loading @@ -244,6 +267,29 @@ m.sendKey("return"); console.assert(m.numberStack[0] === 1.23); }; const rpnTestNotEnoughElementOnStack = function () { const m = new fsm.RPNCalculator(); console.assert(m.numberStack.length === 0); m.sendKey("num1"); m.sendKey("return"); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === 1); m.sendKey("plus"); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === 1); console.assert(m.lastError !== null); m.sendKey("backspace"); console.assert(m.numberStack.length === 0); console.assert(m.lastError === null); m.sendKey("change-sign"); console.assert(m.lastError !== null); m.sendKey("num1"); console.assert(m.lastError === null); m.sendKey("change-sign"); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === -1); console.assert(m.lastError === null); }; /** * run all tests. Loading @@ -266,6 +312,8 @@ rpnTestOperator(); rpnTestNumberErrorHandling(); rpnTestNotEnoughElementOnStack(); rpnBackspaceOperator(); }; /** Loading @@ -275,6 +323,7 @@ */ const updateUI = function (rpnCalculator) { $('#number-display').text(rpnCalculator.currentNumber); $('#error-msg').text(rpnCalculator.lastError); const stack = rpnCalculator.numberStack; const stackSize = stack.length; $("#stack-content").children().remove(); Loading @@ -299,11 +348,12 @@ var i; const target = evt.target; const keyName = $(target).attr('data-bt-name'); if (keyName !== undefined) { console.log("user clicked on button: " + keyName); rpnCalculator.sendKey(keyName); // update UI updateUI(rpnCalculator); } }); }()); </script> Loading fsm.js +88 −10 Original line number Diff line number Diff line Loading @@ -101,6 +101,7 @@ var fsm = function () { "return": "return", change_sign: "change-sign", }; const msgTooFewElementsOnStack = "Too few elements on stack"; this.currentState = stateIdle; this.numberStack = []; Loading Loading @@ -132,13 +133,53 @@ var fsm = function () { console.log(msg); this.lastError = msg; }; /** * clear error msg. */ this.clearErrorMsg = function () { this.lastError = null; }; /** * do calculation for a binary operator. * this handles not enough element in stack problem. * * it may modify this.numberStack and this.lastError. * * Return true on success, return false on failure. * You may do early return if this function fails. this.lastError will * be set when false is returned. */ this.doBinaryOperator = function (func) { const num2 = this.numberStack.pop(); if (num2 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return false; } const num1 = this.numberStack.pop(); if (num1 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); this.numberStack.push(num2); return false; } const result = func(num1, num2); if (result === undefined || result === null || result === false) { // this does consume the two numbers. Just don't push result // to number stack. if (this.lastError === null) { this.setErrorMsg("result of this operator is undefined or null"); } return false; } this.numberStack.push(result); return true; }; /** * return true if given keyName is one of number keys. * number keys include num0-9 and dot. */ const isNumberKey = function (keyName) { return keyName.startsWith("num") || keyName === keyNames.dot; return (keyNames[keyName] !== undefined) && (keyName.startsWith("num") || keyName === keyNames.dot); }; /** * operator key names. Loading Loading @@ -233,36 +274,72 @@ var fsm = function () { } else { switch (keyName) { case keyNames.plus: this.numberStack.push(this.numberStack.pop() + this.numberStack.pop()); if (! this.doBinaryOperator((lhs, rhs) => lhs + rhs)) { return; } break; case keyNames.minus: num2 = this.numberStack.pop(); num1 = this.numberStack.pop(); this.numberStack.push(num1 - num2); if (! this.doBinaryOperator((lhs, rhs) => lhs - rhs)) { return; } break; case keyNames.change_sign: this.numberStack.push(-this.numberStack.pop()); num = this.numberStack.pop(); if (num === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } this.numberStack.push(-num); break; case keyNames.times: this.numberStack.push(this.numberStack.pop() * this.numberStack.pop()); if (! this.doBinaryOperator((lhs, rhs) => lhs * rhs)) { return; } break; case keyNames.divide: num2 = this.numberStack.pop(); num1 = this.numberStack.pop(); this.numberStack.push(num1 / num2); if (! this.doBinaryOperator((lhs, rhs) => { if (rhs === 0) { this.setErrorMsg("Division by 0"); return null; } return lhs / rhs; })) { return; } break; case keyNames.swap: num2 = this.numberStack.pop(); if (num2 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } num1 = this.numberStack.pop(); if (num1 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); this.numberStack.push(num2); return; } this.numberStack.push(num2); this.numberStack.push(num1); break; case keyNames['return']: // duplicate number num = this.numberStack.pop(); if (num === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } this.numberStack.push(num); this.numberStack.push(num); break; case keyNames.backspace: // drop one number on stack num = this.numberStack.pop(); if (num === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } break; default: console.assert(false, "action not implemented, key is " + keyName); } Loading Loading @@ -309,6 +386,7 @@ var fsm = function () { console.assert(false, "unknown state: " + this.currentState); } debug("sendKey " + keyName + ", currentState is " + this.currentState); this.clearErrorMsg(); return this.currentState; }; Loading operational +31 −6 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ Time-stamp: <2017-01-27> | waiting for number or action | press +-*/ | commit number, do op, then idle | | waiting for number or action | press < key | modify number, then waiting for number or action | | idle | press return key | dup number, then idle | | idle | press < key | drop number on stack, then idle | There is no ending state, the machine can always accept new keyboard events. Loading @@ -47,8 +48,6 @@ the parent DOM. * current :entry: ** ** 2017-01-26 handle error for every this.numberStack.pop() ** 2017-01-26 make trail persistent trail is a sized fifo queue, it just store recent 1k entries. when multiple instances of calculator is working. they all write to a single Loading @@ -56,14 +55,40 @@ trail cache. it's not a problem. ** 2017-01-26 make it work offline, add sw.js ** 2017-01-26 make undo work ** 2017-01-26 make stack auto numbering work ** 2017-01-26 make < (backspace) work. ** 2017-01-26 make basic things work. - DONE draw the keyboard using HTML and CSS - make basic number input and arithmetic work - make DUP and SWP work - DONE make basic number input and arithmetic work - DONE make DUP and SWP work - make trail work * done :entry: ** 2017-01-26 make < (backspace) work. ** 2017-01-26 make stack auto numbering work ** 2017-01-26 handle error for every this.numberStack.pop() - handle error for every this.numberStack.pop() - show lastError in web UI. RPNCalculator should clear lastError when it processed another sendKey() successfully. maybe do early return whenever I called this.setErrorMsg(errMsg). - implementation notes - [].pop() return undefined when array is empty. - when fetch 2 values, if only 1 fail. the pop() number should be put back to the stack. add a unit test for this. - about division by 0. js just return infinity. I will show error instead and do not consume any number on stack. ** 2017-01-27 feature: backspace key, when idle, should delete number on stack. ** 2017-01-27 bug: press number-display button trigger an error. it is treated as a num key. the FSM should only accept known keys. update isNumberKey(). ** 2017-01-26 make basic number input and arithmetic work - DONE make event work. - TODO add some visual feedback when clicking a button. like in material design. Loading Loading
calc.html +56 −6 Original line number Diff line number Diff line Loading @@ -54,6 +54,10 @@ border: 1px grey solid; width: 100%; text-align: right; font-size: 1.5em; } #error-msg { height: 30px; } </style> </head> Loading @@ -69,7 +73,10 @@ <div class="keyboard"> <table> <tr> <td colspan="4" data-bt-name="number-display" id="number-display"></td> <td colspan="4" id="error-msg"></td> </tr> <tr> <td colspan="4" id="number-display"></td> </tr> <tr> <td data-bt-name="num7">7</td> Loading Loading @@ -216,6 +223,22 @@ console.assert(m.numberStack[0] === -9); console.assert(m.numberStack[1] === 29 * 2); }; const rpnBackspaceOperator = function () { const m = new fsm.RPNCalculator(); m.sendKey("num1"); m.sendKey("num2"); console.assert(m.currentNumber === "12"); m.sendKey("backspace"); // remove last digit in current number. console.assert(m.currentNumber === "1"); m.sendKey("num3"); m.sendKey("return"); console.assert(m.currentNumber === ""); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === 13); m.sendKey("backspace"); // delete last number in stack. console.assert(m.numberStack.length === 0); }; const rpnTestNumberErrorHandling = function () { var m = new fsm.RPNCalculator(); m.sendKey("num0"); Loading Loading @@ -244,6 +267,29 @@ m.sendKey("return"); console.assert(m.numberStack[0] === 1.23); }; const rpnTestNotEnoughElementOnStack = function () { const m = new fsm.RPNCalculator(); console.assert(m.numberStack.length === 0); m.sendKey("num1"); m.sendKey("return"); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === 1); m.sendKey("plus"); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === 1); console.assert(m.lastError !== null); m.sendKey("backspace"); console.assert(m.numberStack.length === 0); console.assert(m.lastError === null); m.sendKey("change-sign"); console.assert(m.lastError !== null); m.sendKey("num1"); console.assert(m.lastError === null); m.sendKey("change-sign"); console.assert(m.numberStack.length === 1); console.assert(m.numberStack[0] === -1); console.assert(m.lastError === null); }; /** * run all tests. Loading @@ -266,6 +312,8 @@ rpnTestOperator(); rpnTestNumberErrorHandling(); rpnTestNotEnoughElementOnStack(); rpnBackspaceOperator(); }; /** Loading @@ -275,6 +323,7 @@ */ const updateUI = function (rpnCalculator) { $('#number-display').text(rpnCalculator.currentNumber); $('#error-msg').text(rpnCalculator.lastError); const stack = rpnCalculator.numberStack; const stackSize = stack.length; $("#stack-content").children().remove(); Loading @@ -299,11 +348,12 @@ var i; const target = evt.target; const keyName = $(target).attr('data-bt-name'); if (keyName !== undefined) { console.log("user clicked on button: " + keyName); rpnCalculator.sendKey(keyName); // update UI updateUI(rpnCalculator); } }); }()); </script> Loading
fsm.js +88 −10 Original line number Diff line number Diff line Loading @@ -101,6 +101,7 @@ var fsm = function () { "return": "return", change_sign: "change-sign", }; const msgTooFewElementsOnStack = "Too few elements on stack"; this.currentState = stateIdle; this.numberStack = []; Loading Loading @@ -132,13 +133,53 @@ var fsm = function () { console.log(msg); this.lastError = msg; }; /** * clear error msg. */ this.clearErrorMsg = function () { this.lastError = null; }; /** * do calculation for a binary operator. * this handles not enough element in stack problem. * * it may modify this.numberStack and this.lastError. * * Return true on success, return false on failure. * You may do early return if this function fails. this.lastError will * be set when false is returned. */ this.doBinaryOperator = function (func) { const num2 = this.numberStack.pop(); if (num2 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return false; } const num1 = this.numberStack.pop(); if (num1 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); this.numberStack.push(num2); return false; } const result = func(num1, num2); if (result === undefined || result === null || result === false) { // this does consume the two numbers. Just don't push result // to number stack. if (this.lastError === null) { this.setErrorMsg("result of this operator is undefined or null"); } return false; } this.numberStack.push(result); return true; }; /** * return true if given keyName is one of number keys. * number keys include num0-9 and dot. */ const isNumberKey = function (keyName) { return keyName.startsWith("num") || keyName === keyNames.dot; return (keyNames[keyName] !== undefined) && (keyName.startsWith("num") || keyName === keyNames.dot); }; /** * operator key names. Loading Loading @@ -233,36 +274,72 @@ var fsm = function () { } else { switch (keyName) { case keyNames.plus: this.numberStack.push(this.numberStack.pop() + this.numberStack.pop()); if (! this.doBinaryOperator((lhs, rhs) => lhs + rhs)) { return; } break; case keyNames.minus: num2 = this.numberStack.pop(); num1 = this.numberStack.pop(); this.numberStack.push(num1 - num2); if (! this.doBinaryOperator((lhs, rhs) => lhs - rhs)) { return; } break; case keyNames.change_sign: this.numberStack.push(-this.numberStack.pop()); num = this.numberStack.pop(); if (num === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } this.numberStack.push(-num); break; case keyNames.times: this.numberStack.push(this.numberStack.pop() * this.numberStack.pop()); if (! this.doBinaryOperator((lhs, rhs) => lhs * rhs)) { return; } break; case keyNames.divide: num2 = this.numberStack.pop(); num1 = this.numberStack.pop(); this.numberStack.push(num1 / num2); if (! this.doBinaryOperator((lhs, rhs) => { if (rhs === 0) { this.setErrorMsg("Division by 0"); return null; } return lhs / rhs; })) { return; } break; case keyNames.swap: num2 = this.numberStack.pop(); if (num2 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } num1 = this.numberStack.pop(); if (num1 === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); this.numberStack.push(num2); return; } this.numberStack.push(num2); this.numberStack.push(num1); break; case keyNames['return']: // duplicate number num = this.numberStack.pop(); if (num === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } this.numberStack.push(num); this.numberStack.push(num); break; case keyNames.backspace: // drop one number on stack num = this.numberStack.pop(); if (num === undefined) { this.setErrorMsg(msgTooFewElementsOnStack); return; } break; default: console.assert(false, "action not implemented, key is " + keyName); } Loading Loading @@ -309,6 +386,7 @@ var fsm = function () { console.assert(false, "unknown state: " + this.currentState); } debug("sendKey " + keyName + ", currentState is " + this.currentState); this.clearErrorMsg(); return this.currentState; }; Loading
operational +31 −6 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ Time-stamp: <2017-01-27> | waiting for number or action | press +-*/ | commit number, do op, then idle | | waiting for number or action | press < key | modify number, then waiting for number or action | | idle | press return key | dup number, then idle | | idle | press < key | drop number on stack, then idle | There is no ending state, the machine can always accept new keyboard events. Loading @@ -47,8 +48,6 @@ the parent DOM. * current :entry: ** ** 2017-01-26 handle error for every this.numberStack.pop() ** 2017-01-26 make trail persistent trail is a sized fifo queue, it just store recent 1k entries. when multiple instances of calculator is working. they all write to a single Loading @@ -56,14 +55,40 @@ trail cache. it's not a problem. ** 2017-01-26 make it work offline, add sw.js ** 2017-01-26 make undo work ** 2017-01-26 make stack auto numbering work ** 2017-01-26 make < (backspace) work. ** 2017-01-26 make basic things work. - DONE draw the keyboard using HTML and CSS - make basic number input and arithmetic work - make DUP and SWP work - DONE make basic number input and arithmetic work - DONE make DUP and SWP work - make trail work * done :entry: ** 2017-01-26 make < (backspace) work. ** 2017-01-26 make stack auto numbering work ** 2017-01-26 handle error for every this.numberStack.pop() - handle error for every this.numberStack.pop() - show lastError in web UI. RPNCalculator should clear lastError when it processed another sendKey() successfully. maybe do early return whenever I called this.setErrorMsg(errMsg). - implementation notes - [].pop() return undefined when array is empty. - when fetch 2 values, if only 1 fail. the pop() number should be put back to the stack. add a unit test for this. - about division by 0. js just return infinity. I will show error instead and do not consume any number on stack. ** 2017-01-27 feature: backspace key, when idle, should delete number on stack. ** 2017-01-27 bug: press number-display button trigger an error. it is treated as a num key. the FSM should only accept known keys. update isNumberKey(). ** 2017-01-26 make basic number input and arithmetic work - DONE make event work. - TODO add some visual feedback when clicking a button. like in material design. Loading