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 &mdash; 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
&Oslash;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>

Reply via email to