From 01b1b38b1a0015955f6bba2e55318d37bb316953 Mon Sep 17 00:00:00 2001 From: Yuanle Song <sylecn@gmail.com> Date: Fri, 27 Jan 2017 16:34:01 +0800 Subject: [PATCH] make trait work. show scrollbar on stack and trait when content overflows. --- calc.html | 94 ++++++++++++++++++++++++++++++++++++++++++++--------- fifo.js | 6 ++-- fsm.js | 72 ++++++++++++++++++++++++++++++++-------- operational | 33 +++++++++++++++++-- 4 files changed, 171 insertions(+), 34 deletions(-) diff --git a/calc.html b/calc.html index d307835..1db5d51 100644 --- a/calc.html +++ b/calc.html @@ -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); diff --git a/fifo.js b/fifo.js index e58237a..b8809d9 100644 --- a/fifo.js +++ b/fifo.js @@ -1,6 +1,7 @@ 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; diff --git a/fsm.js b/fsm.js index 82855f0..a7fc68f 100644 --- a/fsm.js +++ b/fsm.js @@ -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); diff --git a/operational b/operational index 7fb2db3..5e13b87 100644 --- a/operational +++ b/operational @@ -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() -- GitLab