Commit 44dc983d authored by Yuanle Song's avatar Yuanle Song
Browse files

add error handling for "too few elements on stack".

- feature: backspace should delete number in stack in idle mode
- add error handling for "division by 0"
- bugfix on isNumberKey(), it's more strict now
- show error msg in web UI
parent 0cfc7edb
Loading
Loading
Loading
Loading
+56 −6
Original line number Diff line number Diff line
@@ -54,6 +54,10 @@
	  border: 1px grey solid;
	  width: 100%;
	  text-align: right;
	  font-size: 1.5em;
      }
      #error-msg {
	  height: 30px;
      }
    </style>
  </head>
@@ -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>
@@ -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');
	      if (keyName !== undefined) {
		  console.log("user clicked on button: " + keyName);
		  rpnCalculator.sendKey(keyName);

		  // update UI
		  updateUI(rpnCalculator);
	      }
	  });
      }());
    </script>
+88 −10
Original line number Diff line number Diff line
@@ -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;
    	};

+31 −6
Original line number Diff line number Diff line
@@ -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.