Commit 6721280e authored by Yuanle Song's avatar Yuanle Song
Browse files

fsm.RPNCalculator basics is working perfectly.

added unit test for it.
parent eea788c2
Loading
Loading
Loading
Loading
+119 −17
Original line number Diff line number Diff line
@@ -75,30 +75,31 @@
	      <td data-bt-name="num7">7</td>
	      <td data-bt-name="num8">8</td>
	      <td data-bt-name="num9">9</td>
	      <td data-bt-name="backspace">&lt;</td>
	      <td title="backspace" data-bt-name="backspace">&lt;</td>
	    </tr>
	    <tr>
	      <td data-bt-name="num4">4</td>
	      <td data-bt-name="num5">5</td>
	      <td data-bt-name="num6">6</td>
	      <td data-bt-name="multiply">*</td>
	      <td title="times" data-bt-name="times">*</td>
	    </tr>
	    <tr>
	      <td data-bt-name="num1">1</td>
	      <td data-bt-name="num2">2</td>
	      <td data-bt-name="num3">3</td>
	      <td data-bt-name="subtract">-</td>
	      <td title="minus" data-bt-name="minus">-</td>
	    </tr>
	    <tr>
	      <td data-bt-name="num0">0</td>
	      <td data-bt-name="divide">÷</td>
	      <td data-bt-name="dot">.</td>
	      <td data-bt-name="plus">+</td>
	      <td title="change sign" data-bt-name="change-sign">±</td>
	      <td title="dot" data-bt-name="dot">.</td>
	      <td title="plus" data-bt-name="plus">+</td>
	    </tr>
	    <tr>
	      <td title="swap" data-bt-name="swap">SWAP</td>
	      <td title="undo" data-bt-name="undo">UNDO</td>
	      <td title="confirm or duplicate number" colspan="2" data-bt-name="return">RETURN</td>
	      <td title="divide" data-bt-name="divide">÷</td>
	      <td title="confirm or duplicate number" data-bt-name="return">RETURN</td>
	    </tr>
	  </table>
	</div>
@@ -128,10 +129,100 @@
	      $("#trail-content").append($('<pre>').text('>  3'));
	  };

	  // page init
	  showDumbData();
	  const rpnTestBasic = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 2);
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);
	  };
	  const rpnTestNumberHandling = function () {
	      var m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 12);
	      console.assert(m.currentNumber === "");

	      m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num0");
	      m.sendKey("dot");
	      m.sendKey("num1");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 0.12);
	      console.assert(m.currentNumber === "");

	      m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num0");
	      m.sendKey("dot");
	      m.sendKey("num1");
	      m.sendKey("num2");
	      m.sendKey("backspace");
	      console.assert(m.currentNumber === "0.1");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 0.1);
	      console.assert(m.currentNumber === "");
	  };
	  const rpnTestAutoCommitNumber = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);
	      console.assert(m.currentNumber === "");
	      console.assert(m.currentState === "idle");
	  };
	  const rpnTestOperator = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 2);
	      m.sendKey("minus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === -1);
	      m.sendKey("num3");
	      m.sendKey("num0");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 2);
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 29);
	      m.sendKey("return");    // duplicate
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 29 * 2);
	      m.sendKey("num9");
	      m.sendKey("change-sign");
	      m.sendKey("swap");
	      console.assert(m.numberStack.length === 2);
	      console.assert(m.numberStack[0] === -9);
	      console.assert(m.numberStack[1] === 29 * 2);
	  };

	  /**
	   * run all tests.
	   */
	  const runTests = function () {
	      console.assert(fsm.matchesSubstringAbb("123abb123") === true);
	  console.assert(fsm.matchesSubstringAbb("123ab123") === true);
	      console.assert(fsm.matchesSubstringAbb("123ab123") === false);
	      console.assert(fsm.matchesSubstringAbb("123abaabb123") === true);
	      console.assert(fsm.matchesSubstringAbb("123abbb123") === true);
	      console.assert(fsm.matchesSubstringAbb("123ababab123") === false);
@@ -141,6 +232,17 @@
	      console.assert(fsm.matchesSubstringAbb("123abb") === true);
	      console.assert(fsm.matchesSubstringAbb("abb") === true);

	      rpnTestBasic();
	      rpnTestNumberHandling();
	      rpnTestAutoCommitNumber();
	      rpnTestOperator();
	  };

	  // page init
	  showDumbData();

	  runTests();

	  // setup event handler
	  $('.keyboard td').click(function (evt) {
	      const target = evt.target;
+202 −16
Original line number Diff line number Diff line
@@ -12,37 +12,37 @@ var fsm = function () {
     * matchesSubstringAbb.
     */
    const matchAbbMachine = function () {
	this.stateWaitForA = "wait for a";
	this.stateWaitForFirstB = "wait for 1st b";
	this.stateWaitForSecondB = "wait for 2nd b";
	this.stateSucceed = "succeed";
	const stateWaitForA = "wait for a";
	const stateWaitForFirstB = "wait for 1st b";
	const stateWaitForSecondB = "wait for 2nd b";
	const stateSucceed = "succeed";

	this.currentState = "wait for a";
	this.readch = function (ch) {
	    switch (this.currentState) {
	    case this.stateWaitForA:
	    case stateWaitForA:
		if (ch === 'a') {
		    this.currentState = this.stateWaitForFirstB;
		    this.currentState = stateWaitForFirstB;
		} else {
		    this.currentState = this.stateWaitForA;
		    this.currentState = stateWaitForA;
		}
		break;
	    case this.stateWaitForFirstB:
	    case stateWaitForFirstB:
		if (ch === 'b') {
		    this.currentState = this.stateWaitForSecondB;
		    this.currentState = stateWaitForSecondB;
		} else if (ch === 'a') {
		    // do nothing, keep current state.
		} else {
		    this.currentState = this.stateWaitForA;
		    this.currentState = stateWaitForA;
		}
		break;
	    case this.stateWaitForSecondB:
	    case stateWaitForSecondB:
		if (ch === 'b') {
		    this.currentState = this.stateSucceed;
		    this.currentState = stateSucceed;
		} else if (ch === 'a') {
		    this.currentState = this.stateWaitForFirstB;
		    this.currentState = stateWaitForFirstB;
		} else {
		    this.currentState = this.stateWaitForA;
		    this.currentState = stateWaitForA;
		}
		break;
	    case this.succeed:
@@ -59,7 +59,7 @@ var fsm = function () {
     * implemented using fsm.
     */
    const matchesSubstringAbb = function (str) {
	var machine = new matchAbbMachine();
	const machine = new matchAbbMachine();
	for (i = 0; i < str.length; ++i) {
	    machine.readch(str[i]);
	    if (machine.currentState === "succeed") {
@@ -70,7 +70,193 @@ var fsm = function () {
	return false;
    };

    /**
     * a fsm that implement a RPN calculator. see document in ./operational
     */
    const RPNCalculator = function () {
	// states
    	const stateIdle = "idle";
    	const stateWaitingForNumberOrAction = "waiting for number or action";

	// keynames
	const keyNames = {
	    num0: "num0",
	    num1: "num1",
	    num2: "num2",
	    num3: "num3",
	    num4: "num4",
	    num5: "num5",
	    num6: "num6",
	    num7: "num7",
	    num8: "num8",
	    num9: "num9",
	    dot: "dot",
	    backspace: "backspace",
	    times: "times",
	    divide: "divide",
	    plus: "plus",
	    minus: "minus",
	    swap: "swap",
	    undo: "undo",
	    "return": "return",
	    change_sign: "change-sign",
	};

    	this.currentState = stateIdle;
    	this.numberStack = [];
    	this.currentNumber = "";
	this.lastError = null;

	/**
	 * save current state of this FSM. You can use load to restore the FSM state.
	 */
	this.save = function () {
	    return {
		"currentState": this.currentState,
		"numberStack": this.numberStack,
		"currentNumber": this.currentNumber,
	    };
	};
	/**
	 * load saved data back to this FSM. current FSM status will be lost.
	 */
	this.load = function (data) {
	    this.currentState = data.currentState;
	    this.numberStack = data.numberStack;
	    this.currentNumber = data.currentNumber;
	};
	/**
	 * set error msg. only the last error msg can be fetched via this.lastError.
	 */
	this.setErrorMsg = function (msg) {
	    console.log(msg);
	    this.lastError = msg;
	};

	/**
	 * 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;
	};
	/**
	 * operator key names.
	 */
	const operatorKeyList = [
	    keyNames.change_sign,
	    keyNames.plus, keyNames.minus, keyNames.times, keyNames.divide,
	    keyNames.swap,
	];
	const isOperator = function (keyName) {
	    return operatorKeyList.indexOf(keyName) !== -1;
	};
	/**
	 * keys that is not a simple operator.
	 */
	const miscKeyList = [keyNames["return"], keyNames.undo, keyNames.backspace];
	const isMiscKey = function (keyName) {
	    return miscKeyList.index(keyName) !== -1;
	};
	/**
	 * convert number key to a number string.
	 */
	const numKeyToNumString = function (keyName) {
	    console.assert(isNumberKey(keyName), "not a number key: " + keyName);
	    if (keyName === keyNames.dot) {
		return ".";
	    } else {
		return keyName.substring(3);
	    }
	};
    	this.sendKey = function (keyName) {
	    var num, num1, num2;

    	    switch (this.currentState) {
    	    case stateIdle:
    		if (isNumberKey(keyName)) {
		    // TODO handle errors and special cases
		    this.currentNumber += numKeyToNumString(keyName);
		    this.currentState = stateWaitingForNumberOrAction;
		} else {
		    switch (keyName) {
		    case keyNames.plus:
		    	this.numberStack.push(this.numberStack.pop() + this.numberStack.pop());
		    	break;
		    case keyNames.minus:
			num2 = this.numberStack.pop();
			num1 = this.numberStack.pop();
		    	this.numberStack.push(num1 - num2);
		    	break;
		    case keyNames.change_sign:
		    	this.numberStack.push(-this.numberStack.pop());
		    	break;
		    case keyNames.times:
		    	this.numberStack.push(this.numberStack.pop() * this.numberStack.pop());
		    	break;
		    case keyNames.divide:
			num2 = this.numberStack.pop();
			num1 = this.numberStack.pop();
		    	this.numberStack.push(num1 / num2);
		    	break;
		    case keyNames.swap:
		    	num2 = this.numberStack.pop();
		    	num1 = this.numberStack.pop();
		    	this.numberStack.push(num2);
		    	this.numberStack.push(num1);
		    	break;
		    case keyNames['return']:
			// duplicate number
			num = this.numberStack.pop();
			this.numberStack.push(num);
			this.numberStack.push(num);
		    	break;
		    default:
		    	console.assert(false, "action not implemented, key is " + keyName);
		    }
		}
    		break;
    	    case stateWaitingForNumberOrAction:
    		if (isNumberKey(keyName)) {
		    // TODO handle errors and special cases
		    this.currentNumber += numKeyToNumString(keyName);
		    // keep current state
		} else if (keyName === keyNames['return']) {
		    this.numberStack.push(parseFloat(this.currentNumber));
		    this.currentNumber = "";
		    this.currentState = stateIdle;
		} else if (keyName === keyNames.backspace) {
		    if (this.currentNumber.length > 0) {
			this.currentNumber = this.currentNumber.substring(0, this.currentNumber.length - 1);
		    } else {
			setErrorMsg("no digits to delete");
		    }
		} else if (isOperator(keyName)) {
		    // commit number
		    console.log("send return key automatically");
		    this.sendKey(keyNames['return']);
		    console.assert(this.currentState === stateIdle,
				   "auto commit number should set state to idle");
		    // consume and resend this key. notice the early return.
		    console.log("resend key " + keyName);
		    return this.sendKey(keyName);
		} else {
		    switch (keyName) {
		    default:
			console.assert(false, "action not implemented, key is " + keyName);
		    }
		}
    		break;
	    default:
		console.assert(false, "unknown state: " + this.currentState);
    	    }
    	    debug("sendKey " + keyName + ", currentState is " + this.currentState);
    	    return this.currentState;
    	};
    };

    return {
	matchesSubstringAbb: matchesSubstringAbb
	matchesSubstringAbb: matchesSubstringAbb,
	RPNCalculator: RPNCalculator,
    };
}();
+16 −6
Original line number Diff line number Diff line
* COMMENT -*- mode: org -*-
#+Date: 2017-01-26
Time-stamp: <2017-01-26>
Time-stamp: <2017-01-27>
#+STARTUP: content
* notes                                                               :entry:
* later                                                               :entry:
@@ -15,6 +15,10 @@ the parent DOM.

* current                                                             :entry:
** 
** 2017-01-26 when testing the fsm, always sync this.numberStack and this.currentNumber with the web UI.

** 2017-01-26 handle error for every this.numberStack.pop()

** 2017-01-26 design data structure for the calculator.
- this can be written and tested independently from the web UI.
- basic data:
@@ -24,17 +28,19 @@ the parent DOM.

- how many states are there?

  init state is "waiting for number".
  init state is "idle".

  | this state                   | event                     | next state                                       |
  |------------------------------+---------------------------+--------------------------------------------------|
  | waiting                      | press num keys or dot key | modify number, then waiting for number or action |
  | idle                         | press num keys or dot key | modify number, then waiting for number or action |
  | idle                         | press +-*/ swap           | do op, then idle                                 |
  | waiting for number or action | press num keys or dot key | modify number, then waiting for number or action |
  | waiting for number or action | press return key          | commit number, then waiting                      |
  | waiting for number or action | press +-*/                | commit number, do op, then waiting               |
  | waiting for number or action | press return key          | commit number, then idle                         |
  | 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                            |

  There is no "ending state", the machine can always accept new keyboard events.
  There is no ending state, the machine can always accept new keyboard events.

- a few display and error handling.

@@ -67,6 +73,10 @@ trail cache. it's not a problem.
- make DUP and SWP work
- make trail work
* done                                                                :entry:
** 2017-01-26 what to do with negate operator?
emacs use n (change sign)
add this button. move divide button to somewhere else.

** 2017-01-26 how to write a fsm simulator in javascript.
- try write an easier example before writing the real thing.
- easy example: