improved JavaScript/DHTML RPN calculator/formula editor
Kragen Sitaker
kragen at pobox.com
Thu Mar 16 00:25:06 EST 2006
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>
More information about the Kragen-hacks
mailing list