The online version of this is, or will be, at http://pobox.com/~kragen/sw/js-calc.html
Now it depends on MochiKit, so to run it locally, you'll have to edit the <script> tag for MochiKit to point to somewhere that MochiKit exists. FORTH-like, I added a second stack to allow more flexibility in how you manipulate values, and changed backspace to banish a value to the bottom of that second stack, reducing the chances of accidentally losing your work. I've posted two previous versions of this: April 2005 "JavaScript/DHTML RPN calculator/formula editor" http://lists.canonical.org/pipermail/kragen-hacks/2005-April/000408.html March 2006 "improved JavaScript/DHTML RPN calculator/formula editor" http://lists.canonical.org/pipermail/kragen-hacks/2006-March/000428.html <html><head><title>Javascript/DHTML Reverse Polish Calculator</title><!-- See text at end for details, or just type random numbers and letters for a while. TODO: D graphing - more compact display (e.g. "0.333..." instead of 0.3333333333333333, perhaps with mouseover to expand, perhaps with pull-down menu in mouseover) - tabular display? - resizing graphs! D avoiding letting top-of-stack get out of view - undo! X LRU move-to-front, along the lines of alt-tab window switching - graph construction - or at least parenthesis removal - parenthesis removal requires some kind of rule for operator precedence; once that's in place, the rule is "parenthesize any subexpressions less tightly bound" - for graph construction, I'm currently thinking: (x + x) / y where x = iota 3 + 1 y = 6 rather than the current (((iota 3) + 1) + ((iota 3) + 1)) / 6 although originally I was thinking of a more graphical display instead - the ability to change values inside existing expressions - and, in effect, define them as functions - try out my idea for having *functions* on the stack rather than just concrete numerical values - complex numbers - APL-style array operations (present in limited form) - these lose the expressions that birthed them, though - units - labels - programmability --> <style type="text/css"> #output div, #rstack div, input { padding: 1px; width: 60% } #output div, #rstack div { border: 1px solid lightslategrey; margin: 1px 0 } .instructions { float: right; width: 35%; color: darkslategrey; background-color: wheat; padding: 0.5em; font-size: smaller } input { border: 1px solid yellow; font: inherit; margin: 0 } .expr { color: darkslategrey; font-size: smaller } </style> <script src="/home/kragen/pkgs/MochiKit-1.3.1/lib/MochiKit/MochiKit.js"> </script> <script type="text/javascript"> // JavaScript calculator. Depends on MochiKit. ///////// Debugging stuff // dom node containing table displaying object properties function domdump(obj) { var align = {valign: 'top'} return TABLE(null, map(function(prop) { return TR(null, TD(align, repr(prop)), TD(align, repr(obj[prop]))) }, keys(obj))) } // subset of an object function slice(obj, props) { var rv = {} for (var i = 0; i < props.length; i++) rv[props[i]] = obj[props[i]] return rv } // display relevant properties of an event function debug_output(key) { var out = $('debug_output') if (!out) return replaceChildNodes(out, domdump(key)) //out.appendChild(domdump(slice(key, ['isChar', 'keyCode', 'charCode', // 'shiftKey', 'type']))) } ///////// Calculator primitive stuff // At first it seemed like a good idea to just use DOM nodes for the // calculator's stack; now that each stack item has three attributes, // it seems like a less good idea. // return the value from the top of the stack (can't fail) function pop_stack() { var tos = $('output').lastChild if (!tos) return {value: '0', expr: 0, atomic: 1} // crude hack var rv_value = tos.firstChild.nodeValue var rv_expr = tos.firstChild.nextSibling.nextSibling.firstChild.nodeValue var atomic = tos.getAttribute('atomic') $('output').removeChild(tos) return {value: rv_value, expr: rv_expr, atomic: atomic} } function discard_value() { var tos = $('output').lastChild if (!tos) return $('output').removeChild(tos) $('rstack').appendChild(tos) } function go_down() { var tors = $('rstack').firstChild if (!tors) return $('rstack').removeChild(tors) $('output').appendChild(tors) } function drag_down() { var tors = $('rstack').firstChild if (!tors) return var tos = $('output').lastChild if (!tos) return go_down() $('rstack').removeChild(tors) $('output').insertBefore(tors, $('output').lastChild) } function go_up() { var tos = $('output').lastChild if (!tos) return $('output').removeChild(tos) $('rstack').insertBefore(tos, $('rstack').firstChild) } function drag_up() { var tos = $('output').lastChild if (!tos) return var next = tos.previousSibling if (!next) return go_up() $('output').removeChild(next) $('rstack').insertBefore(next, $('rstack').firstChild) } function isFinite(number) { if (isNaN(number)) return false if (number == Infinity) return false if (number == -Infinity) return false return true } // put a graph of an array into a node function graph(val, node) { var w = 200 var h = 20 var canvas = CANVAS({width: w, height: h}) if (!canvas.getContext) return // no canvas support! node.appendChild(canvas) var ctx = canvas.getContext('2d') // determine coordinate transformation var ww = w + 1 var hh = h + 1 var per_sample = ww / val.length var minval = listMin(concat([0], filter(isFinite, val))) var maxval = listMax(concat([1], filter(isFinite, val))) var size = maxval - minval var vertical_xform = function(datum) { // maybe I should have the canvas do this? return h - (datum - minval) * h / size } // x-axis ctx.strokeStyle = 'grey' ctx.moveTo(0, vertical_xform(0)) ctx.lineTo(ww, vertical_xform(0)) // plot points ctx.strokeStyle = 'black' var lastPoint = false forEach(range(val.length), function(ii) { if (!isFinite(val[ii])) { lastPoint = false return } var x = per_sample * (ii + 0.5) var y = vertical_xform(val[ii]) if (lastPoint) { ctx.lineTo(x, y) } else { ctx.moveTo(x, y) } lastPoint = true }) ctx.stroke() } // push a value function push_stack(val) { var n = DIV(null, val.value, SPAN(' = '), SPAN({'class': 'expr'}, val.expr)) var tograph = asarray('' + val.value) if (tograph.length > 1) graph(tograph, n) if (val.atomic) n.setAttribute('atomic', 1) $('output').appendChild(n) } ///////// Calculator user operations // Split apart an expression into its "predecessors". Being really // cheap-ass and storing all historical values at the moment: var predecessors = ({}) function split_value() { var value = pop_stack() //debug_output(predecessors) var preds = predecessors[value.expr] if (preds == null) { // Maybe we should yank stuff off the bottom of the lower stack instead? push_stack_with_preds([value], {value: value.expr + ' is atomic', expr: 'split ' + get_expr(value)}) return } forEach(preds, push_stack) } function push_stack_with_preds(preds, val) { predecessors[val.expr] = preds push_stack(val) } // before you invoke any operation, or when you hit Enter, push // current text field onto stack (if any) function append_output() { // "atomic" means no parens are ever needed around this expression var input = $('the_input') if (strip(input.value) == '') return push_stack({value: input.value, expr: input.value, atomic: 1}) $('the_input').value = '' return false } // is the input field empty? function empty_field() { return (strip($('the_input').value) == '') } function handle_backspace() { if (empty_field()) { discard_value() return false } else return true } // get properly wrapped value function get_expr(obj) { if (obj.atomic) return obj.expr return "(" + obj.expr + ")" } function asarray(astring) { // if (!astring.indexOf) alert(astring) astring = strip(astring) // I was using new Number(astr) here, but it turns out that // new Number('45') != new Number('45') and that was causing // problems when trying to compute the max or min of a vector. return map(function(astr){return parseFloat(astr)}, astring.split(/\s+/)) } function bin_apply_fun(fun, y, x) { var yy = asarray(y) var xx = asarray(x) if (yy.length == 1) { yy = list(repeat(yy[0], xx.length)) } else if (xx.length == 1) { xx = list(repeat(xx[0], yy.length)) } if (xx.length != yy.length) { return "Error: mismatched lengths (" + y + " and " + x + ")" } return map(fun, yy, xx).join(' ') } // binary numerical operation function bin_num_op(fun, op) { append_output() var x = pop_stack() var y = pop_stack() push_stack_with_preds([y, x], {value: bin_apply_fun(fun, y.value, x.value), expr: get_expr(y) + " " + op + " " + get_expr(x)}) return false } // unary numerical operation function un_num_op(fun, op) { append_output() var v = pop_stack() push_stack_with_preds([v], {value: map(fun, asarray(v.value)).join(' '), expr: op + get_expr(v)}) return false } function iota() { append_output() var v = pop_stack() push_stack_with_preds([v], {value: list(range(v.value)).join(' '), expr: 'iota ' + get_expr(v)}) return false } function explode() { append_output() var v = pop_stack() var vv = asarray(v.value) if (vv.length > 1) { var val = vv.shift() push_stack({value: val, expr: val, atomic: 1}) push_stack_with_preds([v], {value: vv.join(' '), expr: 'tail ' + get_expr(v)}) } else { push_stack(v) push_stack({value: "Error: can't explode a scalar", expr: '@' + v.value, atomic: 1}) } return false } function make_array() { append_output() var a = pop_stack() var b = pop_stack() push_stack_with_preds([b, a], {value: [b.value, a.value].join(' '), expr: get_expr(b) + ", " + get_expr(a)}) return false } // swap top two items on stack. function swap_stack() { var x = pop_stack() var y = pop_stack() push_stack(x) push_stack(y) } var binary_operators = { '+': operator.add, '*': operator.mul, '/': operator.div, '-': operator.sub, '^': Math.pow, } var unary_operators = { e: [Math.exp, 'exp '], l: [Math.log, 'ln '], a: [Math.atan, 'arctan '], s: [Math.sin, 'sin '], c: [Math.cos, 'cos '], r: [function(x){return 1/x}, '1/'], _: [function(x){return -x}, '-'], } function scroll_input_into_view() { var inp = $('the_input') window.scrollTo(0, elementPosition(inp).y + $(inp).scrollHeight/2 - document.body.clientHeight/2) } // called when a printable key is pressed and released in the input field function handle_keypress(evt) { log('press', evt.key().toSource()) var key = evt.key() var code = key.code var chr = key.string //debug_output(key) if (chr in binary_operators) { bin_num_op(binary_operators[chr], chr) } else if (chr in unary_operators) { var op = unary_operators[chr] un_num_op(op[0], op[1]) } else if (chr == 'i') { // 'i' for 'iota' iota() } else if (chr == ',') { // ',' to append arrays make_array() } else if (chr == '@') { // '@' to expand one explode() } else if (chr == '.' || chr == ' ' || chr >= '0' && chr <= '9') { return } else if (code == 0) { // some special key //debug_output(evt._event) // We can't reliably prevent Enter or Tab from propagating normally // in the places where their UI actions are handled, so we do it here: var prim = evt._event var mod = evt.modifier() if (!mod.any && (prim.keyCode == 13 || prim.keyCode == 9)) { } else if (prim.keyCode == prim.DOM_VK_DOWN) { // Also, if we try to handle 'up' and 'down' in keyup or // keydown, we lose auto-repeat. if (mod.shift && !mod.ctrl && !mod.alt && !mod.meta) drag_down() else if (!mod.any) go_down() else return } else if (prim.keyCode == prim.DOM_VK_UP) { if (mod.shift && !mod.ctrl && !mod.alt && !mod.meta) drag_up() else if (!mod.any) go_up() else return } else return } else if (chr == 'z') { // "undo" split_value() } // if we didn't return somewhere along the way, stop the propagation // of the event: evt.stop() scroll_input_into_view() } function handle_keyup(evt) { //debug_output(merge({'up': 1}, key)) log('up', evt.key().toSource()) if (evt.key().string == 'KEY_ENTER') { // XXX: I don't remember why I thought it was better to handle this in // keyup instead of keydown. if (empty_field()) { var val = pop_stack() push_stack(val) push_stack(val) } append_output() // This doesn't work here if the enters are generated by key // repeat, so we do it in handle_keypress instead: // evt.stop() } scroll_input_into_view() } function handle_keydown(evt) { var string = evt.key().string log('down', evt.key().toSource()) // We must handle KEY_TAB in keydown instead of keyup because, if we // don't cancel it, we lose focus and never get the keyup. if (string == 'KEY_TAB') { append_output() swap_stack() // This has the same problem as stopping Enter in handle_keyup, // and the same workaround: // evt.stop() } else if (string == 'KEY_BACKSPACE') { // If we handle backspace in keyup, then the field may already // have been emptied, which means the backspace would have both // deleted a digit and the top of the stack. // In this case, not handling auto-repeated backspace is kind of a // bonus: if (!handle_backspace()) evt.stop() } scroll_input_into_view() } function startup() { $('the_input').focus() connect('the_input', 'onkeypress', handle_keypress) connect('the_input', 'onkeyup', handle_keyup) connect('the_input', 'onkeydown', handle_keydown) } </script></head><body onload="startup()"> <!-- prototype layout for DAG expressions: <table> <tr><td colspan="3">(x + x) / y</td></tr> <tr><td> </td><td>where </td><td>x = iota 3 + 1</td></tr> <tr><td></td><td></td> <td>y = 6</td></tr> </table> --> <div class="instructions"><b>Graphing reverse Polish notation SIMD calculator.</b><br /> Tab to swap<br /> Enter to dup<br /> Backspace to dismiss a value (send it to the bottom)<br /> Up and Down to move through the values<br /> Shift-Up and Shift-Down to move a value</br> z to split apart an expression<br /> <p>+*/- for arithmetic<br> ^ for power<br> r for reciprocal<br> _ to change sign<br> e for exponential<br> L for natural log<br> s for sine<br> c for cosine<br> a for arctangent (in radians)<br> </p> <p> Space to separate numbers in an array<br> i for a range of numbers (APL iota)<br> @ to explode an array onto the stack<br> , to combine the top two stack items<br> </p> <p>You can compute most common functions this way. log base 10 is "L10L/", sin of degrees is "1A4*180/*S", cube root is "L3/E" or "3r^". I used techniques like this to find that 1/((arctan (.5 / (exp ((ln (1 - (.5 ^ 2))) / 2)))) / ((arctan 1) * 4)) is 6, which was sort of what I was hoping — I managed to take the arcsine of 0.5. For a good time, try "40", Enter, Enter, "i", Tab, "/1a4*4**s2^". </p> <p>Many JavaScript RPN calculators already exist, like <a href="http://www.naveen.net/calculator/">Alexander Rau's</a>, <a href="http://www.arachnoid.com/lutusp/calculator.html">P. Lutus's</a>, <a href="http://en.tldp.org/linuxfocus/common/src/article319/rpnjcalc.html">Guido Socher's</a>, <a href="http://users.aol.com/jgrochow/html/calc.html">Jerrold Grochow's</a>, <a href="http://home.att.net/%7Esrschmitt/script_reverse_polish.html">Stephen R. Schmitt's</a>, <a href="http://hp.vector.co.jp/authors/VA004808/rpncalc.html">H. Tanuma's</a>, <a href="http://www3.brinkster.com/Redline/toys/rpn.asp">Roland Stolfa's</a>, <a href="http://dspace.dial.pipex.com/town/square/gd86/calc.htm">Nigel Bromley's</a>, <a href="http://www.danbbs.dk/%7Eerikoest/rpn.htm">Erik Ãstergaard's</a>, and <a href="http://www.google.com/search?q=javascript+rpn">many others</a>. This one differs from the others by being nicer-looking (if perhaps harder to figure out how to use), more pleasant to use (being entirely keyboard-driven and instantly responsive), supporting vectors and automatic graphing, being less powerful than a few of them but more powerful than most, having no hidden state, and showing the provenance of each value. I'm thinking that this one would fit nicely in a bookmarklet, and I have other plans up my sleeve as well. </p> </div> <div id="output"></div> <form name="theform"><input name="bejeweled" id="the_input"></form> <div id="rstack"></div> <a href="http://lists.canonical.org/pipermail/kragen-hacks/2005-April/000408.html">posted to kragen-hacks in April 2005</a> and <a href="http://lists.canonical.org/pipermail/kragen-hacks/2006-March/000428.html">again in March 2006</a>. <p>I don't think it works in Safari or MSIE. Use <a href="http://www.getfirefox.com/">Firefox</a>.</p> <div id="debug_output">debug output goes here</div> </body></html>