improved JavaScript/DHTML RPN calculator/formula editor
Now it graphs. http://pobox.com/ragen/sw/js-calc.html Javascript/DHTML Reverse Polish Calculator #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 } / 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
JavaScript/DHTML RPN calculator/formula editor
http://pobox.com/~kragen/sw/js-calc.html Javascript/DHTML Reverse Polish Calculator #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 } / 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 } // display relevant properties of an event function debug_output(key) { var out = document.getElementById('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 o = document.getElementById('output') var tos = o.lastChild if (!tos) return {value: 0, expr: 0, atomic: 1} // crude hack var rv_value = new Number(tos.firstChild.nodeValue) var rv_expr = tos.lastChild.firstChild.nodeValue var atomic = tos.getAttribute('atomic') o.removeChild(tos) return {value: rv_value, expr: rv_expr, atomic: atomic} } // 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) if (val.atomic) n.setAttribute('atomic', 1) document.getElementById('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 + ")" } // 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: fun(y.value, x.value), expr: get_expr(y) + " " + op + " " + get_expr(x)}) return false } // unary numerical operation function un_num_op(fun, op) { if (!empty_field()) append_output() var v = pop_stack() push_stack({value: fun(v.value), expr: op + get_expr(v)}) 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 fals