Loading calc.html +79 −15 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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> Loading Loading @@ -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 () { Loading @@ -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(); Loading Loading @@ -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; Loading @@ -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); Loading fifo.js +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 = []; Loading @@ -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; Loading fsm.js +58 −14 Original line number Diff line number Diff line Loading @@ -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", Loading @@ -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. Loading @@ -116,6 +141,7 @@ var fsm = function () { "currentState": this.currentState, "numberStack": this.numberStack, "currentNumber": this.currentNumber, "trail": this.trail, }; }; /** Loading @@ -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. Loading @@ -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); Loading @@ -171,6 +198,8 @@ var fsm = function () { return false; } this.numberStack.push(result); this.trail.push([shortKeyName(keyName), result]); return true; }; Loading Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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 Loading @@ -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); Loading operational +30 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading Loading
calc.html +79 −15 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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> Loading Loading @@ -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 () { Loading @@ -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(); Loading Loading @@ -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; Loading @@ -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); Loading
fifo.js +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 = []; Loading @@ -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; Loading
fsm.js +58 −14 Original line number Diff line number Diff line Loading @@ -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", Loading @@ -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. Loading @@ -116,6 +141,7 @@ var fsm = function () { "currentState": this.currentState, "numberStack": this.numberStack, "currentNumber": this.currentNumber, "trail": this.trail, }; }; /** Loading @@ -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. Loading @@ -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); Loading @@ -171,6 +198,8 @@ var fsm = function () { return false; } this.numberStack.push(result); this.trail.push([shortKeyName(keyName), result]); return true; }; Loading Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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 Loading @@ -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); Loading
operational +30 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading