Skip to content
calc.html 19.2 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;
Yuanle Song's avatar
Yuanle Song committed
	  border: 1px skyblue solid;
      }
      #container > div {
	  /* border: 1px grey dotted; */
      }
      #container > div > p {
Yuanle Song's avatar
Yuanle Song committed
	  margin: 10px;
      }
      #stack {
	  width: 200px;
Yuanle Song's avatar
Yuanle Song committed
      #stack-content {
	  margin-left: 10px;
	  font-size: 1.2em;
	  height: 400px;
	  overflow: auto;
      }
      #expand-stack {
	  display: none;
      }
      #keyboard {
	  width: 300px;
	  height: 100%;
	  border-left: 1px skyblue dotted;
	  border-right: 1px skyblue dotted;
      }
      #trail {
	  width: 200px;
      }
      #trail-content {
	  margin-left: 10px;
	  font-size: 1.2em;
Yuanle Song's avatar
Yuanle Song committed
	  font-family: monospace;
	  height: 400px;
	  overflow: auto;
Yuanle Song's avatar
Yuanle Song committed
      #expand-trail {
	  display: none;
      }
      .keyboard {
	  font-size: 1.2em;
	  text-align: center;
	  padding-bottom: 10px;
      }
      .keyboard table {
	  margin: 2px auto;
Yuanle Song's avatar
Yuanle Song committed
	  border-collapse: collapse;
      }
      .keyboard td {
	  width: 60px;
	  height: 60px;
Yuanle Song's avatar
Yuanle Song committed
	  border: 1px skyblue solid;
      }
      #number-display {
Yuanle Song's avatar
Yuanle Song committed
	  font-family: monospace;
	  border: 1px skyblue solid;
	  width: 100%;
Yuanle Song's avatar
Yuanle Song committed
	  max-width: 150px;	/*just set to something small and it works in chrome.*/
	  overflow: auto;
	  text-align: right;
Yuanle Song's avatar
Yuanle Song committed
	  padding: 0 10px;
      }
      #error-msg {
	  height: 30px;
Yuanle Song's avatar
Yuanle Song committed
	  border-style: none;
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;
      }
Yuanle Song's avatar
Yuanle Song committed
@media only screen and (max-device-width: 480px) {
      #container {
	  width: 300px;
	  flex-flow: column;
      }
      #stack {
	  height: 100%;
	  width: 300px;
	  order: 200;
      }
      #stack-content {
	  height: 100%;
	  max-height: 100px;
      }
      #expand-stack {
	  display: inline;
      }
      #keyboard {
	  height: 100%;
	  order: 100;
      }
      #trail {
	  height: 100%;
	  width: 300px;
	  order: 300;
      }
      #trail-content {
	  height: 100%;
	  max-height: 100px;
      }
      #expand-trail {
	  display: inline;
      }
      #error-msg {
	  height: 20px;
      }
      .keyboard td {
	  width: 60px;
	  height: 50px;
      }
}
    </style>
  </head>

  <body>
    <div id="container">
      <div id="stack">
Yuanle Song's avatar
Yuanle Song committed
	<p>Stack <a title="clear stack" id="clear-stack" href="#">Clear Stack</a> <a title="expand stack" id="expand-stack" href="#">Expand 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="delete pending digit or number in stack" data-bt-name="backspace"></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 top two numbers on stack" data-bt-name="swap">SWAP</td>
	      <td title="undo last operator" data-bt-name="undo">UNDO</td>
	      <td title="divide" data-bt-name="divide">÷</td>
	      <td title="push pending number to stack or duplicate top number on stack"
		  data-bt-name="return"></td>
	    <tr>
	      <td title="sum all numbers in stack" data-bt-name="sum-all">Σ</td>
	      <td title="square root" data-bt-name="square-root"></td>
	      <td title="power" data-bt-name="power">^</td>
	      <td></td>
	    </tr>
	  </table>
	</div>
      </div>
      <div id="trail">
Yuanle Song's avatar
Yuanle Song committed
	<p>Trail <a title="clear trail" id="clear-trail" href="#">Clear Trail</a> <a title="expand trail" id="expand-trail" href="#">Expand 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
	      // for (i = 0; i < 10; ++i) {
	      // 	  rpnCalculator.sendKey("num" + i);
	      // 	  rpnCalculator.sendKey("num" + i);
	      // }
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 rpnTestBackspaceOperator = 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 rpnTestSumAllOperator = function () {
	      const m = new fsm.RPNCalculator();
	      m.sendKey("sum-all");
	      console.assert(m.lastError !== null);
	      m.sendKey("num1");
	      m.sendKey("sum-all");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 1);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("return");
	      m.sendKey("num3");
	      m.sendKey("return");
	      m.sendKey("num1");
	      m.sendKey("change-sign");
	      m.sendKey("return");
	      m.sendKey("sum-all");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 5);
	  };
	  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);
	  };
Yuanle Song's avatar
Yuanle Song committed
	  const rpnTestUndo = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      console.assert(m.snapshots.data.length === 1);
	      console.assert(m.snapshots.data[0].numberStack.length === 0);
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.snapshots.data.length === 2);
	      console.assert(m.snapshots.data[0].numberStack.length === 0);
	      console.assert(m.snapshots.data[1].numberStack.length === 1);
	      console.assert(m.snapshots.data[1].numberStack[0] === 1);

	      console.assert(m.numberStack.length === 2);
	      console.assert(m.numberStack[0] === 1);
	      console.assert(m.numberStack[1] === 2);

	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 1);
	      m.sendKey("num3");
	      m.sendKey("return");
	      m.sendKey("plus");
	      m.sendKey("undo");
	      m.sendKey("times");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);
	      m.sendKey("undo");
	      m.sendKey("minus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === -2);
	  };
	  const rpnTestUndoWithErrors = function () {
	      var m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 0);

	      m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num8");
	      m.sendKey("return");
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num0");
	      m.sendKey("divide");
	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 3);
	      console.assert(m.numberStack[0] === 8);
	      console.assert(m.numberStack[1] === 1);
	      console.assert(m.numberStack[2] === 0);
	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 2);
	      console.assert(m.numberStack[0] === 8);
	      console.assert(m.numberStack[1] === 1);
	  };

	  /**
	   * 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();
	      rpnTestBackspaceOperator();
	      rpnTestSumAllOperator();
Yuanle Song's avatar
Yuanle Song committed

	      rpnTestUndo();
	      rpnTestUndoWithErrors();
	  /**
	   * 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(9000); // TODO how to scroll to bottom?
Yuanle Song's avatar
Yuanle Song committed

	      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) {
Yuanle Song's avatar
Yuanle Song committed
		  $("#trail-content").append(trailTable).scrollTop(9000); // TODO how to scroll to bottom?
Yuanle Song's avatar
Yuanle Song committed
	      }
	  // 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;
	  });
Yuanle Song's avatar
Yuanle Song committed
	  $('#expand-stack').click(function (evt) {
	      const oldMaxHeight = $('#stack-content').css("max-height");
	      if (oldMaxHeight === "100px") {
		  $('#stack-content').css("max-height", "300px");
		  $('#expand-stack').text("Shrink Stack");
	      } else {
		  $('#stack-content').css("max-height", "100px");
		  $('#expand-stack').text("Expand Stack");
	      }
	  });
	  $('#expand-trail').click(function (evt) {
	      const oldMaxHeight = $('#trail-content').css("max-height");
	      if (oldMaxHeight === "300px") {
		  $('#trail-content').css("max-height", "100px");
		  $('#expand-trail').text("Expand Trail");
	      } else {
		  $('#trail-content').css("max-height", "300px");
		  $('#expand-trail').text("Shrink Trail");
	      }
	  });
      }());
    </script>
  </body>

</html>