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>