diff --git a/calc.html b/calc.html index fe3f1dc42d9fb563de92e0c8563f0af7bec4b65e..919de2514cc52e86984be3c3cd4926c56eef8eee 100644 --- a/calc.html +++ b/calc.html @@ -54,6 +54,10 @@ border: 1px grey solid; width: 100%; text-align: right; + font-size: 1.5em; + } + #error-msg { + height: 30px; } @@ -69,7 +73,10 @@
| + | + | ||||||
| 7 | @@ -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"); @@ -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. @@ -266,6 +312,8 @@ rpnTestOperator(); rpnTestNumberErrorHandling(); + rpnTestNotEnoughElementOnStack(); + rpnBackspaceOperator(); }; /** @@ -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(); @@ -299,11 +348,12 @@ var i; const target = evt.target; const keyName = $(target).attr('data-bt-name'); - console.log("user clicked on button: " + keyName); - rpnCalculator.sendKey(keyName); - - // update UI - updateUI(rpnCalculator); + if (keyName !== undefined) { + console.log("user clicked on button: " + keyName); + rpnCalculator.sendKey(keyName); + // update UI + updateUI(rpnCalculator); + } }); }()); diff --git a/fsm.js b/fsm.js index 59c800f34824cc72609018d46dcd2896d0e6b4c9..82855f0853d96707d22250761876fb5a060edea6 100644 --- a/fsm.js +++ b/fsm.js @@ -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 = []; @@ -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. @@ -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); } @@ -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; }; diff --git a/operational b/operational index efe05920f74796c6ab7d68e4600e78cc4e46ac57..b9991c482abe7c1577dd06628d738656c87162d2 100644 --- a/operational +++ b/operational @@ -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. @@ -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 @@ -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.|||||||