Now it graphs. http://pobox.com/ragen/sw/js-calc.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. to-do: D graphing - more compact display - avoiding letting top-of-stack get out of view - undo! - LRU move-to-front, along the lines of alt-tab window switching - graph construction - or at least parenthesis removal - the ability to change values inside existing expressions - and define them as functions - 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, input { padding: 1px; width: 60% } #output 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> ///////// Debugging stuff function td(textcontent) { var rv = document.createElement('TD') rv.appendChild(document.createTextNode(textcontent)) return rv } // table row containing two pieces of text function tablerow(a, b) { var rv = document.createElement('TR') rv.appendChild(td(a)) rv.appendChild(td(b)) return rv } // dom node containing table displaying object properties function domdump(obj) { var rv = document.createElement('TABLE') for (var prop in obj) rv.appendChild(tablerow(prop, obj[prop])) return rv } // 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 } function $(id) { return document.getElementById(id) } // display relevant properties of an event function debug_output(key) { var out = $('debug_output') if (!out) return out.removeChild(out.firstChild) out.appendChild(domdump(slice(key, ['isChar', 'keyCode', 'charCode', 'shiftKey', 'type']))) // out.appendChild(domdump(key)) } ///////// 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. // find the input we're using for this function thefield() { return document.forms.theform.the_input } // 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 min(values) { var rv = values[0] for (var ii = 0; ii < values.length; ii++) { if (values[ii] < rv) rv = values[ii] } return rv } function max(values) { var rv = values[0] for (var ii = 0; ii < values.length; ii++) { if (values[ii] > rv) rv = values[ii] } return rv } function isFinite(number) { if (isNaN(number)) return false if (number == Infinity) return false if (number == -Infinity) return false return true } function filter(fun, array) { var rv = [] for (var ii = 0; ii < array.length; ii++) { if (fun(array[ii])) rv.push(array[ii]) } return rv } // put a graph of an array into a node function graph(val, node) { var canvas = document.createElement('canvas') if (!canvas.getContext) return // no canvas support! var w = 200 var h = 20 canvas.setAttribute('width', w) canvas.setAttribute('height', h) 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 = min(filter(isFinite, val)) if (minval > 0) minval = 0 var maxval = max(filter(isFinite, val)) if (maxval < 1) maxval = 1 var range = maxval - minval var vertical_xform = function(datum) { // maybe I should have the canvas do this? return h - (datum - minval) * h / range } // 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 for (var ii = 0; ii < val.length; ii++) { if (!isFinite(val[ii])) { lastPoint = false continue } 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 = document.createElement('DIV') n.appendChild(document.createTextNode(val.value)) var eq = document.createElement('SPAN') eq.appendChild(document.createTextNode(' = ')) n.appendChild(eq) var m = document.createElement('SPAN') m.className = 'expr' m.appendChild(document.createTextNode(val.expr)) n.appendChild(m) var tograph = asarray('' + val.value) if (tograph instanceof Array) graph(tograph, n) if (val.atomic) n.setAttribute('atomic', 1) $('output').appendChild(n) } ///////// Calculator user operations // before you invoke any operation, or when you hit Enter, push // current text field onto stack function append_output() { // "atomic" means no parens are ever needed around this expression var val = {value: thefield().value, expr: thefield().value, atomic: 1} if (val.value == '') { val = pop_stack() push_stack(val) } push_stack(val) thefield().value = '' return false } // is the input field empty? function empty_field() { return (thefield().value == '') } function handle_backspace(key) { if (empty_field()) { pop_stack() return false } else return true } // get properly wrapped value function get_expr(obj) { if (obj.atomic) return obj.expr return "(" + obj.expr + ")" } function map(fun, array) { var rv = [] for (var ii = 0; ii < array.length; ii++) { rv.push(fun(array[ii])) } return rv } function asarray(astring) { // if (!astring.indexOf) alert(astring) if (astring.indexOf(' ') == -1) return new Number(astring) return map(function(astr){return new Number(astr)}, astring.split(' ')) } function repeat_scalar(scalar, ntimes) { var rv = [] for (var ii = 0; ii < ntimes; ii++) { rv.push(scalar) } return rv } function bin_apply_fun(fun, y, x) { var yy = asarray(y) var xx = asarray(x) if (!(yy instanceof Array) && !(xx instanceof Array)) { return fun(yy, xx) } else if (!(yy instanceof Array)) { yy = repeat_scalar(yy, xx.length) } else if (!(xx instanceof Array)) { xx = repeat_scalar(xx, yy.length) } if (xx.length != yy.length) { return "Error: mismatched lengths (" + y + " and " + x + ")" } var rv = [] for (var ii = 0; ii < yy.length; ii++) { rv.push(fun(yy[ii], xx[ii])) } return rv.join(' ') } // binary numerical operation function bin_num_op(fun, op) { if (!empty_field()) append_output() var x = pop_stack() var y = pop_stack() push_stack({value: bin_apply_fun(fun, y.value, x.value), expr: get_expr(y) + " " + op + " " + get_expr(x)}) return false } function un_apply_fun(fun, v) { var vv = asarray(v) if (vv instanceof Array) { return map(fun, vv).join(' ') } else { return fun(vv) } } // unary numerical operation function un_num_op(fun, op) { if (!empty_field()) append_output() var v = pop_stack() push_stack({value: un_apply_fun(fun, v.value), expr: op + get_expr(v)}) return false } function iota() { if (!empty_field()) append_output() var v = pop_stack() var rv = [] for (var ii = 0; ii < v.value; ii++) { rv.push(ii) } push_stack({value: rv.join(' '), expr: 'iota ' + get_expr(v)}) return false } function explode() { if (!empty_field()) append_output() var v = pop_stack() var vv = asarray(v.value) if (vv instanceof Array) { var val = vv.shift() push_stack({value: val, expr: val, atomic: 1}) push_stack({value: vv.join(' '), expr: vv.join(' '), atomic: 1}) } else { push_stack(v) push_stack({value: "Error: can't explode a scalar", expr: '@' + v.value, atomic: 1}) } return false } function make_array() { if (!empty_field()) append_output() var a = pop_stack() var b = pop_stack() push_stack({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) } // called when a non-modifier key is pressed and released in the input field function handle_keypress(key) { var code = key.keyCode var chr = key.charCode debug_output(key) // I tried rewriting this with tables, and it was more complicated and // no shorter. if (code == key.DOM_VK_RETURN) { append_output() return false } else if (code == key.DOM_VK_BACK_SPACE) { return handle_backspace(key) } else if (chr == 43) { // + return bin_num_op(function(a, b){return a + b}, '+') } else if (chr == 42) { // * return bin_num_op(function(a, b){return a * b}, '*') } else if (chr == 47) { // '/' return bin_num_op(function(a, b){return a / b}, '/') } else if (chr == 45) { // - return bin_num_op(function(a, b){return a - b}, '-') } else if (chr == 94) { // ^ return bin_num_op(Math.pow, '^') } else if (code == 9) { // tab if (!empty_field()) append_output() swap_stack() return false } else if (chr == 69 || chr == 101) { // e return un_num_op(Math.exp, 'exp ') } else if (chr == 76 || chr == 108) { // L return un_num_op(Math.log, 'ln ') } else if (chr == 65 || chr == 97) { // a return un_num_op(Math.atan, 'arctan ') } else if (chr == 83 || chr == 115) { // s return un_num_op(Math.sin, 'sin ') } else if (chr == 67 || chr == 99) { // c return un_num_op(Math.cos, 'cos ') } else if (chr == 82 || chr == 114) { // r return un_num_op(function(x){return 1/x}, '1/') } else if (chr == 95) { // _ return un_num_op(function(x){return -x}, '-') } else if (chr == 105) { // 'i' for 'iota' return iota() } else if (chr == 44) { // ',' to append arrays return make_array() } else if (chr == 64) { // '@' to expand one return explode() } else if (chr == 46) { // . return true } else if (chr == 32) { // space, used for vectors return true } else if (chr >= 48 && chr < 58) { // 0-9 return true } else if (chr == 0) { // some special key // other keys we might care about here are // DOM_VK_LEFT, DOM_VK_RIGHT, DOM_VK_UP, DOM_VK_DOWN return true } else return false } function startup() { thefield().focus() thefield().onkeypress = handle_keypress } </script> </head><body onLoad="startup()"> <div class="instructions"><p>Reverse Polish calculator.<br /> +*/- for arithmetic<br /> ^ for power<br /> Tab to swap<br /> Enter to dup<br /> Backspace to delete<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/~srschmitt/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/~erikoest/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="the_input"/></form> <a href="http://lists.canonical.org/pipermail/kragen-hacks/2005-April/000408.html" >posted to kragen-hacks in April 2005</a> and again in March 2006. <p>No, it doesn't work in Safari. Or MSIE.</p> <!-- <div id="debug_output">debug output goes here</div> --> </body></html>