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; */
#stack-content {
margin-left: 10px;
font-size: 1.2em;
height: 400px;
overflow: auto;
}
#expand-stack {
display: none;
}
height: 100%;
border-left: 1px skyblue dotted;
border-right: 1px skyblue dotted;
}
#trail-content {
margin-left: 10px;
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;
max-width: 150px; /*just set to something small and it works in chrome.*/
overflow: auto;
}
#error-msg {
height: 30px;
min-width: 10px;
text-align: left;
}
.trail-active td.trail-op {
border-left: 2px lightblue solid;
}
.overlay {
display: none;
}
#help-overlay-container {
width: 700px;
}
#help-overlay-content {
}
#help-overlay-content h4 {
}
.button-group {
width: 100%;
display: flex;
justify-content: center;
}
/* title with buttons on the right. see ./test-rhs-link-on-title-row.html */
.title {
display: flex;
/* border: 1px black solid; */
}
.title h3 {
margin: 8px 16px;
font-size: 1em;
}
.title-button-group {
flex-grow: 1;
display: flex;
justify-content: flex-end;
margin: 8px 1em;
}
.title-button-group a, .title-button-group button {
padding: 0 5px;
}
#help-title {
text-align: center;
}
#copyright {
text-align: center;
}
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
@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;
}
}
#help-overlay-content {
</style>
</head>
<body>
<div id="container">
<div id="stack">
<div class="title">
<h3>Stack</h3>
<div class="title-button-group">
<a title="clear stack" id="clear-stack" href="#">Clear</a>
<a title="expand stack" id="expand-stack" href="#">Expand</a>
<div id="stack-content"></div>
</div>
<div id="keyboard">
<div class="title">
<h3>Keyboard</h3>
<div class="title-button-group">
<a title="reset RPN calculator" id="reset" href="#">Reset All</a>
<a title="help (?)" id="help-link" href="#">Help</a>
</div>
</div>
<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 last number in stack (q)" 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="push pending number to stack or duplicate top number on stack (Enter)"
data-bt-name="return">⏎</td>
<td title="change sign (n)" 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 (U)" data-bt-name="undo">UNDO</td>
<td title="divide (/)" data-bt-name="divide">÷</td>
<td title="minus" data-bt-name="minus">-</td>
<tr>
<td title="sum all numbers in stack" data-bt-name="sum-all">Σ</td>
<td title="square root (Q)" data-bt-name="square-root">√</td>
<td title="power (^)" data-bt-name="power">^</td>
<td title="round (R)" data-bt-name="round">ROUND</td>
</table>
</div>
</div>
<div id="trail">
<div class="title">
<h3>Trail</h3>
<div class="title-button-group">
<a title="clear trail" id="clear-trail" href="#">Clear</a>
<a title="expand trail" id="expand-trail" href="#">Expand</a>
<div id="trail-content"></div>
</div>
</div>
<div id="help-overlay-container" class="overlay">
<div id="help-overlay-content">
<h3 id="help-title" style="text-align: center">RPN calculator v1.0.0</h3>
<h4>About</h4>
<p>RPN calculator is a web implementation of a <a href="#TODO">reverse polish notation calculator</a>. You push numbers to stack first, then press operator keys which will pop number from stack and run the calculation. For example, you may press num1, return, num2, num3, return, plus, and you will see result 24 in top of stack. Number is pushed to stack using return key.</p>
<p>Supported operators are: +, -, *, /, square-root, power, sum-all. Utility features include delete number, duplicate number, swap top 2 number on stack, undo, clear stack. Hover over button to see what that button does and the hotkey for that button.</p>
<p>This program should work on desktop and mobile browser. Tested in chrome and firefox.</p>
<h4>Credit</h4>
<p>This project is inspired by <a href="#TODO">emacs calc mode</a>. It tries to mimic the behavior when possible.</p>
<h4>Debug Features</h4>
<p>This website use service worker to make it work offline. If the local cache does not work for some reason, you may <a title="refresh page" id="refresh-page" href="#">force refresh page</a> to get latest version. You may also <a title="purge service worker cache" id="purge-service-worker-cache" href="#">purge all service worker cache</a>.</p>
<p id="copyright">Copyright (C) 2017 Yuanle Song <sylecn@gmail.com></p>
</div>
</div>
</div>
<script type="text/javascript" src="vendor/localforage.min.js"></script>
<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 -->
const testing = false;
const uiTesting = false;
const enableServiceWorker = false;
if ('serviceWorker' in navigator && enableServiceWorker) {
navigator.serviceWorker
.register('sw.js', {scope: './'})
.then(function() {
console.log("Service Worker Registered");
});
}
/**
* show dumb data for UI testing.
*/
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");
// for (i = 0; i < 10; ++i) {
// rpnCalculator.sendKey("num" + i);
// rpnCalculator.sendKey("num" + i);
// }
// $("#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);
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);
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
};
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);
};
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
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);
};
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
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();
rpnTestNotEnoughElementOnStack();
rpnTestBackspaceOperator();
rpnTestSumAllOperator();
/**
* update web UI to match the state of th rpnCalculator.
*
* currently it syncs rpnCalculator.currentNumber and rpnCalculator.numberStack.
*/
const updateUI = function (rpnCalculator) {
$('#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]));
}
$("#stack-content").append($('<pre>').text(' .')).scrollTop(9000); // 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();
$("#trail-content").append(trailTable).scrollTop(9000); // TODO how to scroll to bottom?
const rpnCalculator = new fsm.RPNCalculator();
if (uiTesting) {
showDumbData(rpnCalculator);
}
// init web UI
updateUI(rpnCalculator);
// setup event handler
$('.keyboard td').click(function (evt) {
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;
});
const showHelpOverlay = function () {
$('#container').hide();
$('#help-overlay-container').slideDown();
};
const hideHelpOverlay = function () {
$('#help-overlay-container').hide();
$('#container').show();
};
$('#help-link').click(function (evt) {
return false;
});
$('.close-help-overlay').click(function (evt) {
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
$('#refresh-page').click(function (evt) {
const urlJoin = function (baseUrl, path) {
const lastSlash = baseUrl.lastIndexOf("/");
return baseUrl.substring(0, lastSlash + 1) + path;
};
console.assert(urlJoin("http://localhost:8000/calc.html", "fifo.js") ===
"http://localhost:8000/fifo.js");
console.assert(urlJoin("http://localhost:8000/", "fifo.js") ===
"http://localhost:8000/fifo.js");
console.assert(urlJoin("http://localhost:8000/abc/t1.html", "fifo.js") ===
"http://localhost:8000/abc/fifo.js");
console.assert(urlJoin("https://www.example.com/calc.html", "fifo.js") ===
"https://www.example.com/fifo.js");
console.assert(urlJoin("https://www.example.com/", "fifo.js") ===
"https://www.example.com/fifo.js");
// set service worker cache to expire, thus fetching latest page on next refresh.
const htmlUrl = window.location.href;
const fifoJsUrl = urlJoin(htmlUrl, 'fifo.js');
const fsmJsUrl = urlJoin(htmlUrl, 'fsm.js');
// clear cache in page load order. the basic dependence should
// be cleared first before trigger a page reload.
localforage.setItem('timestamp:' + fsmJsUrl, new Date(2016,0,1).toJSON()).then(function (_) {
localforage.setItem('timestamp:' + fifoJsUrl, new Date(2016,0,1).toJSON()).then(function (_) {
localforage.setItem('timestamp:' + htmlUrl, new Date(2016,0,1).toJSON()).then(function (_) {
window.location.href = window.location.href;
});
});
});
return false;
});
$('#purge-service-worker-cache').click(function (evt) {
localforage.clear().then(function (_) {
caches.delete('calc').then(function (_) {
const msg = "clear all done";
console.log(msg);
$('#error-msg').text(msg);
$('#help-overlay-container').hide();
});
});
});
$('#expand-stack').click(function (evt) {
const oldMaxHeight = $('#stack-content').css("max-height");
if (oldMaxHeight === "100px") {
$('#stack-content').css("max-height", "300px");
} else {
$('#stack-content').css("max-height", "100px");
});
$('#expand-trail').click(function (evt) {
const oldMaxHeight = $('#trail-content').css("max-height");
if (oldMaxHeight === "300px") {
$('#trail-content').css("max-height", "100px");
} else {
$('#trail-content').css("max-height", "300px");
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
$(document).keypress(function (evt) {
// map evt.key to RPNCalculator keyName.
const keyMap = {
"1": "num1",
"2": "num2",
"3": "num3",
"4": "num4",
"5": "num5",
"6": "num6",
"7": "num7",
"8": "num8",
"9": "num9",
"0": "num0",
".": "dot",
"Enter": "return",
"+": "plus",
"-": "minus",
"*": "times",
"/": "divide",
"n": "change-sign",
"^": "power",
"Q": "square-root",
"U": "undo",
// below is not from emacs calc
"q": "backspace",
};
console.log("charCode=" + evt.charCode + ", key=" + evt.key);
console.log(typeof(evt.key));
const keyName = keyMap[evt.key];
if (keyName !== undefined) {
rpnCalculator.sendKey(keyName);
updateUI(rpnCalculator);
} else {
if (evt.key === "?") {
const helpVisible = $('#help-overlay-container').css("display") === "block";
if (helpVisible) {
hideHelpOverlay();
} else {
showHelpOverlay();
}
</script>
</body>
</html>