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>&nbsp;&nbsp;</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 &#8212; 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>

Reply via email to