Skip to content
calc.html 13.8 KiB
Newer Older
<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>RPN calculator</title>
    <style type="text/css">
      #container {
	  display: flex;
	  flex-flow: row;
	  width: 700px;
	  margin: 10px auto;
	  border: 1px grey dotted;
      }
      #container > div {
	  border: 1px grey dotted;
      }
      #container > div > p {
	  margin-left: 10px;
      }
      #stack {
	  width: 200px;
Yuanle Song's avatar
Yuanle Song committed
	  height: 500px;
      }
      #keyboard {
	  width: 300px;
Yuanle Song's avatar
Yuanle Song committed
	  height: 500px;
      }
      #trail {
	  width: 200px;
Yuanle Song's avatar
Yuanle Song committed
	  height: 500px;
      }
      #stack-content {
	  margin-left: 10px;
	  font-size: 1.2em;
Yuanle Song's avatar
Yuanle Song committed
	  height: 400px;
	  overflow: auto;
      }
      #trail-content {
	  margin-left: 10px;
	  font-size: 1.2em;
Yuanle Song's avatar
Yuanle Song committed
	  font-family: monospace;
	  height: 400px;
	  overflow: auto;
      }
      .keyboard {
	  font-size: 1.2em;
	  text-align: center;
      }
      .keyboard table {
	  margin: 2px auto;
      }
      .keyboard td {
	  width: 60px;
	  height: 60px;
      }
      #number-display {
	  border: 1px grey solid;
	  width: 100%;
	  text-align: right;
	  font-size: 1.5em;
      }
      #error-msg {
	  height: 30px;
Yuanle Song's avatar
Yuanle Song committed
      .trail-op {
	  min-width: 40px;
Yuanle Song's avatar
Yuanle Song committed
	  text-align: right;
      }
      .trail-num {
	  padding-left: 10px;
Yuanle Song's avatar
Yuanle Song committed
	  min-width: 10px;
	  text-align: left;
      }
      .trail-active td.trail-op {
	  border-left: 2px lightblue solid;
      }
    </style>
  </head>

  <body>
    <div id="container">
      <div id="stack">
	<p>Stack <a title="clear stack" id="clear-stack" href="#">Clear Stack</a></p>
	<div id="stack-content"></div>
      </div>
      <div id="keyboard">
	<p>Keyboard <a title="reset RPN calculator" id="reset" href="#">Reset All</a></p>
	<div class="keyboard">
	  <table>
	    <tr>
	      <td colspan="4" id="error-msg"></td>
	    </tr>
	    <tr>
	      <td colspan="4" id="number-display"></td>
	      <td data-bt-name="num7">7</td>
	      <td data-bt-name="num8">8</td>
	      <td data-bt-name="num9">9</td>
	      <td title="backspace" data-bt-name="backspace">&lt;</td>
	      <td data-bt-name="num4">4</td>
	      <td data-bt-name="num5">5</td>
	      <td data-bt-name="num6">6</td>
	      <td title="times" data-bt-name="times">*</td>
	      <td data-bt-name="num1">1</td>
	      <td data-bt-name="num2">2</td>
	      <td data-bt-name="num3">3</td>
	      <td title="minus" data-bt-name="minus">-</td>
	      <td data-bt-name="num0">0</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>
	      <td title="swap" data-bt-name="swap">SWAP</td>
	      <td title="undo" data-bt-name="undo">UNDO</td>
	      <td title="divide" data-bt-name="divide">÷</td>
	      <td title="confirm or duplicate number" data-bt-name="return">RETURN</td>
	    </tr>
	  </table>
	</div>
      </div>
      <div id="trail">
	<p>Trail <a title="clear trail" id="clear-trail" href="#">Clear Trail</a></p>
	<div id="trail-content"></div>
      </div>
    </div>
    <script type="text/javascript" src="vendor/jquery.min.js"></script>
    <script type="text/javascript" src="fifo.js"></script>
    <script type="text/javascript" src="fsm.js"></script> <!-- fifo.js is required by fsm.js -->
    <script type="text/javascript">
      (function () {
	  /**
	   * show dumb data for UI testing.
	   */
Yuanle Song's avatar
Yuanle Song committed
	  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");
Yuanle Song's avatar
Yuanle Song committed
	      // $("#number-display").text('789');
Yuanle Song's avatar
Yuanle Song committed
	      // $("#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 () {
	      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);
Yuanle Song's avatar
Yuanle Song committed

	      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();
	      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);
	  };
	  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");
	      m.sendKey("num0");
	      console.assert(m.currentNumber === "0");

	      m = new fsm.RPNCalculator();
	      m.sendKey("dot");
	      console.assert(m.currentNumber === "0.");
	      m.sendKey("backspace");
	      console.assert(m.currentNumber === "0");
	      m.sendKey("backspace");
	      console.assert(m.currentNumber === "");

	      m = new fsm.RPNCalculator();
	      m.sendKey("num1");
	      m.sendKey("dot");
	      m.sendKey("num2");
	      console.assert(m.currentNumber === "1.2");
	      m.sendKey("dot");    // future dot is ignored and show error.
	      console.assert(m.currentNumber === "1.2");
	      m.sendKey("num3");
	      console.assert(m.currentNumber === "1.23");
	      m.sendKey("dot");
	      console.assert(m.currentNumber === "1.23");
	      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.
	   */
	  const runTests = function () {
	      console.assert(fsm.matchesSubstringAbb("123abb123") === 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);
	      console.assert(fsm.matchesSubstringAbb("") === false);
	      console.assert(fsm.matchesSubstringAbb("123") === false);
	      console.assert(fsm.matchesSubstringAbb("123ab") === false);
	      console.assert(fsm.matchesSubstringAbb("123abb") === true);
	      console.assert(fsm.matchesSubstringAbb("abb") === true);

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

	      rpnTestNumberErrorHandling();
	      rpnTestNotEnoughElementOnStack();
	      rpnBackspaceOperator();
	  /**
	   * update web UI to match the state of th rpnCalculator.
	   *
	   * currently it syncs rpnCalculator.currentNumber and rpnCalculator.numberStack.
	   */
	  const updateUI = function (rpnCalculator) {
Yuanle Song's avatar
Yuanle Song committed
	      var trailOp, trailNum, trNode;

	      $('#number-display').text(rpnCalculator.currentNumber);
	      $('#error-msg').text(rpnCalculator.lastError);
	      const stack = rpnCalculator.numberStack;
	      const stackSize = stack.length;
	      $("#stack-content").children().remove();
	      for (i = 0; i < stackSize; ++i) {
		  $("#stack-content").append($('<pre>').text((stackSize - i) + ': ' + stack[i]));
	      }
Yuanle Song's avatar
Yuanle Song committed
	      $("#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);
	      }
	      $("#trail-content").children().remove();
Yuanle Song's avatar
Yuanle Song committed
	      if (trailSize > 0) {
		  $("#trail-content").append(trailTable).scrollTop(900); // TODO how to scroll to bottom?
	      }
	  // page init
	  const rpnCalculator = new fsm.RPNCalculator();

Yuanle Song's avatar
Yuanle Song committed
	  // showDumbData(rpnCalculator);
	  // init web UI
	  updateUI(rpnCalculator);

	  // setup event handler
	  $('.keyboard td').click(function (evt) {
	      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);
	      }
	  $('#clear-trail').click(function (evt) {
	      rpnCalculator.clearTrail();
	      updateUI(rpnCalculator);
	      return false;
	  });
	  $('#clear-stack').click(function (evt) {
	      rpnCalculator.clearStack();
	      updateUI(rpnCalculator);
	      return false;
	  });
	  $('#reset').click(function (evt) {
	      rpnCalculator.reset();
	      updateUI(rpnCalculator);
	      return false;
	  });
      }());
    </script>
  </body>

</html>