Commit 01b1b38b authored by Yuanle Song's avatar Yuanle Song
Browse files

make trait work.

show scrollbar on stack and trait when content overflows.
parent 6c54fb04
Loading
Loading
Loading
Loading
+79 −15
Original line number Diff line number Diff line
@@ -21,23 +21,28 @@
      }
      #stack {
	  width: 200px;
	  height: 500px
	  height: 500px;
      }
      #keyboard {
	  width: 300px;
	  height: 500px
	  height: 500px;
      }
      #trail {
	  width: 200px;
	  height: 500px
	  height: 500px;
      }
      #stack-content {
	  margin-left: 10px;
	  font-size: 1.2em;
	  height: 400px;
	  overflow: auto;
      }
      #trail-content {
	  margin-left: 10px;
	  font-size: 1.2em;
	  font-family: monospace;
	  height: 400px;
	  overflow: auto;
      }
      .keyboard {
	  font-size: 1.2em;
@@ -59,6 +64,17 @@
      #error-msg {
	  height: 30px;
      }
      .trail-op {
	  min-width: 10px;
	  text-align: right;
      }
      .trail-num {
	  min-width: 10px;
	  text-align: left;
      }
      .trail-active td.trail-op {
	  border-left: 2px lightblue solid;
      }
    </style>
  </head>

@@ -124,17 +140,36 @@
	  /**
	   * show dumb data for UI testing.
	   */
	  const showDumbData = function () {
	      $("#number-display").text('789');
	  const showDumbData = function (rpnCalculator) {
	      var i;
	      rpnCalculator.sendKey("num1");
	      rpnCalculator.sendKey("return");
	      rpnCalculator.sendKey("num2");
	      rpnCalculator.sendKey("return");
	      rpnCalculator.sendKey("num3");
	      rpnCalculator.sendKey("return");
	      for (i = 0; i < 30; ++i) {
		  rpnCalculator.sendKey("return");
		  rpnCalculator.sendKey("plus");
	      }
	      for (i = 0; i < 30; ++i) {
		  rpnCalculator.sendKey("return");
	      }

	      rpnCalculator.sendKey("num7");
	      rpnCalculator.sendKey("num8");
	      rpnCalculator.sendKey("num9");

	      $("#stack-content").append($('<pre>').text('3: 1'));
	      $("#stack-content").append($('<pre>').text('2: 2'));
	      $("#stack-content").append($('<pre>').text('1: 3'));
	      $("#stack-content").append($('<pre>').text('   .'));
	      // $("#number-display").text('789');

	      $("#trail-content").append($('<pre>').text('   1'));
	      $("#trail-content").append($('<pre>').text('   2'));
	      $("#trail-content").append($('<pre>').text('>  3'));
	      // $("#stack-content").append($('<pre>').text('3: 1'));
	      // $("#stack-content").append($('<pre>').text('2: 2'));
	      // $("#stack-content").append($('<pre>').text('1: 3'));
	      // $("#stack-content").append($('<pre>').text('   .'));

	      // $("#trail-content").append($('<pre>').text('   1'));
	      // $("#trail-content").append($('<pre>').text('   2'));
	      // $("#trail-content").append($('<pre>').text('>  3'));
	  };

	  const rpnTestBasic = function () {
@@ -148,6 +183,15 @@
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);

	      const trail = m.trail.data;    // the underlying array.
	      console.assert(trail.length === 3);
	      console.assert(trail[0][0] === null);
	      console.assert(trail[0][1] === 1);
	      console.assert(trail[1][0] === null);
	      console.assert(trail[1][1] === 2);
	      console.assert(trail[2][0] === '+');
	      console.assert(trail[2][1] === 3);
	  };
	  const rpnTestNumberHandling = function () {
	      var m = new fsm.RPNCalculator();
@@ -323,6 +367,8 @@
	   * currently it syncs rpnCalculator.currentNumber and rpnCalculator.numberStack.
	   */
	  const updateUI = function (rpnCalculator) {
	      var trailOp, trailNum, trNode;

	      $('#number-display').text(rpnCalculator.currentNumber);
	      $('#error-msg').text(rpnCalculator.lastError);
	      const stack = rpnCalculator.numberStack;
@@ -331,16 +377,34 @@
	      for (i = 0; i < stackSize; ++i) {
		  $("#stack-content").append($('<pre>').text((stackSize - i) + ': ' + stack[i]));
	      }
	      $("#stack-content").append($('<pre>').text('   .'));
	      $("#stack-content").append($('<pre>').text('   .')).scrollTop(900); // TODO how to scroll to bottom?

	      const trail = rpnCalculator.trail.data;
	      const trailSize = trail.length;
	      const trailTable = $('<table>');
	      for (i = 0; i < trailSize; ++i) {
		  trailOp = trail[i][0];
		  trailNum = trail[i][1];
		  trNode = $('<tr>');
		  if (i === trailSize - 1) {
		      trNode.addClass("trail-active");
		  }
		  trNode.append($('<td>').text(trailOp).addClass("trail-op"));
		  trNode.append($('<td>').text(trailNum).addClass("trail-num"));
		  trailTable.append(trNode);
	      }
	      if (trailSize > 0) {
		  $("#trail-content").children().remove();
		  $("#trail-content").append(trailTable).scrollTop(900); // TODO how to scroll to bottom?
	      }
	  };

	  // page init
	  // showDumbData();

	  runTests();

	  const rpnCalculator = new fsm.RPNCalculator();

	  // showDumbData(rpnCalculator);
	  // init web UI
	  updateUI(rpnCalculator);

+4 −2
Original line number Diff line number Diff line
var fifo = function () {
    /**
     * an unbounded fifo queue.
     * an unbounded fifo queue. Use push and pop to add or remove elements.
     * you can also read the raw elements in this.data.
     */
    const Queue = function () {
	this.data = [];
@@ -16,7 +17,8 @@ var fifo = function () {
    };

    /**
     * a bounded fifo queue.
     * a bounded fifo queue. Use push and pop to add or remove elements.
     * you can also read the raw elements in this.data.
     */
    const BoundedQueue = function (capacity) {
	this.capacity = capacity;
+58 −14
Original line number Diff line number Diff line
@@ -78,7 +78,7 @@ var fsm = function () {
    	const stateIdle = "idle";
    	const stateWaitingForNumberOrAction = "waiting for number or action";

	// keynames
	// define constants for key name literals.
	const keyNames = {
	    num0: "num0",
	    num1: "num1",
@@ -101,12 +101,37 @@ var fsm = function () {
	    "return": "return",
	    change_sign: "change-sign",
	};
	// map keyName to short key name.
	const shortKeyNames = {
	    "num0": "0",
	    "num1": "1",
	    "num2": "2",
	    "num3": "3",
	    "num4": "4",
	    "num5": "5",
	    "num6": "6",
	    "num7": "7",
	    "num8": "8",
	    "num9": "9",
	    "dot": ".",
	    "backspace": "bksp",
	    "times": "*",
	    "divide": "/",
	    "plus": "+",
	    "minus": "-",
	    "swap": "swap",
	    "undo": "undo",
	    "return": "ret",
	    "change-sign": "chs",
	};
	const msgTooFewElementsOnStack = "Too few elements on stack";
	const trailHistorySize = 50;

    	this.currentState = stateIdle;
    	this.numberStack = [];
    	this.currentNumber = "";
	this.lastError = null;
	this.trail = new fifo.BoundedQueue(trailHistorySize);

	/**
	 * save current state of this FSM. You can use load to restore the FSM state.
@@ -116,6 +141,7 @@ var fsm = function () {
		"currentState": this.currentState,
		"numberStack": this.numberStack,
		"currentNumber": this.currentNumber,
		"trail": this.trail,
	    };
	};
	/**
@@ -125,6 +151,7 @@ var fsm = function () {
	    this.currentState = data.currentState;
	    this.numberStack = data.numberStack;
	    this.currentNumber = data.currentNumber;
	    this.trail = data.trail;
	};
	/**
	 * set error msg. only the last error msg can be fetched via this.lastError.
@@ -149,7 +176,7 @@ var fsm = function () {
	 * You may do early return if this function fails. this.lastError will
	 * be set when false is returned.
	 */
	this.doBinaryOperator = function (func) {
	this.doBinaryOperator = function (keyName, func) {
	    const num2 = this.numberStack.pop();
	    if (num2 === undefined) {
		this.setErrorMsg(msgTooFewElementsOnStack);
@@ -171,6 +198,8 @@ var fsm = function () {
		return false;
	    }
	    this.numberStack.push(result);

	    this.trail.push([shortKeyName(keyName), result]);
	    return true;
	};

@@ -253,6 +282,13 @@ var fsm = function () {
	    console.assert(r[0] === "1.2");
	    console.assert(r[1] !== null);
	};
	/**
	 * return short key name for given keyName.
	 * this is used to show operator in shorter form in trails.
	 */
	const shortKeyName = function (keyName) {
	    return shortKeyNames[keyName];
	};
    	this.sendKey = function (keyName) {
	    var num, num1, num2;
	    var numString, errMsg;
@@ -273,31 +309,33 @@ var fsm = function () {
		    this.currentState = stateWaitingForNumberOrAction;
		} else {
		    switch (keyName) {
		    case keyNames.plus:
			if (! this.doBinaryOperator((lhs, rhs) => lhs + rhs)) {
		    case keyNames.change_sign:
			num = this.numberStack.pop();
			if (num === undefined) {
			    this.setErrorMsg(msgTooFewElementsOnStack);
			    return;
			}
		    	this.numberStack.push(-num);
			console.log("trail push " + [keyName, shortKeyName(keyName), -num]);
			this.trail.push([shortKeyName(keyName), -num]);
		    	break;
		    case keyNames.minus:
			if (! this.doBinaryOperator((lhs, rhs) => lhs - rhs)) {
		    case keyNames.plus:
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs + rhs)) {
			    return;
			}
		    	break;
		    case keyNames.change_sign:
			num = this.numberStack.pop();
			if (num === undefined) {
			    this.setErrorMsg(msgTooFewElementsOnStack);
		    case keyNames.minus:
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs - rhs)) {
			    return;
			}
		    	this.numberStack.push(-num);
		    	break;
		    case keyNames.times:
			if (! this.doBinaryOperator((lhs, rhs) => lhs * rhs)) {
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs * rhs)) {
			    return;
			}
		    	break;
		    case keyNames.divide:
			if (! this.doBinaryOperator((lhs, rhs) => {
			if (! this.doBinaryOperator(keyName, (lhs, rhs) => {
			    if (rhs === 0) {
				this.setErrorMsg("Division by 0");
				return null;
@@ -321,6 +359,7 @@ var fsm = function () {
			}
		    	this.numberStack.push(num2);
		    	this.numberStack.push(num1);
			// no changes on trail
		    	break;
		    case keyNames['return']:
			// duplicate number
@@ -331,6 +370,7 @@ var fsm = function () {
			}
			this.numberStack.push(num);
			this.numberStack.push(num);
			// no changes on trail.
		    	break;
		    case keyNames.backspace:
			// drop one number on stack
@@ -357,9 +397,13 @@ var fsm = function () {
		    }
		    // keep current state
		} else if (keyName === keyNames['return']) {
		    this.numberStack.push(parseFloat(this.currentNumber));
		    // commit number to stack
		    num = parseFloat(this.currentNumber);
		    this.numberStack.push(num);
		    this.currentNumber = "";
		    this.currentState = stateIdle;

		    this.trail.push([null, num]);
		} else if (keyName === keyNames.backspace) {
		    if (this.currentNumber.length > 0) {
			this.currentNumber = this.currentNumber.substring(0, this.currentNumber.length - 1);
+30 −3
Original line number Diff line number Diff line
@@ -48,24 +48,43 @@ the parent DOM.

* current                                                             :entry:
** 
** 2017-01-27 add a reset button. browser refresh button is not always easily accessible, esp on mobile.
** 2017-01-27 make trail work
- when is trail being updated?
  - when a number is committed to number stack. return.
  - when a new number is pushed to stack as an op result. plus, change-sign.
  - swap doesn't change trail.
  - dup and swap doesn't change trail.

- this should be part of the FSM.
  what data should I store?
  it's a queue of [(Maybe op, number)].

  as mentioned previously, this data can be made persistent across
  pages/sessions.
- first write a bounded fifo queue in javascript.

- DONE first write a bounded fifo queue in javascript.
- load persistent data for trails when init the FSM.
- update the persistent data when trails has changed.
  update the persistent data when trails has changed.

  // is updating local storage a heavy operation? I hope it doesn't sync to
  disk everytime I update a key.

- implementation notes
  - show last 10 entries in UI. allow user to use scrollbar to scroll to
    earlier entries.

    leave this UI optimization later. for now, just show last 10 or 20 entries.
  - use table to show trail in HTML.
    trailOp right align.
    trailNum left align.
    mark last entry using color highlight. there is no need for > marker.

  - grep for "this.numberStack.push" to push elements to this.trail.

  - change-sign op is not shown in trait. why?
    it's because shortKeyNames key should be key name string literal, not the
    js constant identifier.

** 2017-01-26 make trail persistent
trail is a bounded fifo queue, it just store recent 1k entries.
when multiple instances of calculator is working. they all write to a single
@@ -79,6 +98,14 @@ trail cache. it's not a problem.
- DONE make DUP and SWP work
- make trail work
* done                                                                :entry:
** 2017-01-27 stack, trail: overflow problem.
- when there is not enough room, show scrollbar.
- always show the title section.
- auto scroll to bottom when content overflow.

  TODO how to scroll to bottom?
  $('#stack-content').scroll

** 2017-01-26 make < (backspace) work.
** 2017-01-26 make stack auto numbering work
** 2017-01-26 handle error for every this.numberStack.pop()