Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* Summable tables, version [0.0.1a]
Originally from http://en.wikipedia.org/wiki/User:Splarka/summabletables.js
 
Framework for automatically calculating totals for table columns.

-------------------------------- Control classes --------------------------------
  CLASS                 TAGS    EFFECT
 summable              table   Defines summable table
 summable-row-skip     tr      Defines rows to skip calculating on
 summable-row-result   tr      Defines result/output rows (optional)
 summable-row-cols     tr      Defines the row defining the columns (optional)
 summable-col          td/th   Defines a summable column (summable-row-cols only)

Usage:
* COLSPAN AND ROWSPAN WILL PROBABLY BREAK SHIT, except on skipped rows
* Usually defining a summable-row-cols isn't necessary, unless you want to use a colspan on the top row.
** All cells in summable-row-cols with summable-col are determined to define a sortable column (numerically).
*** Using colspan will screw this up. 
**** Possibly this could be worked around, but rowspan couldn't without storing the whole damn table, ugh.
* Leaving off a defined summable-row-result allows non-javascript users to not see an empty row.
** If generating a result row, and if the first cell isn't summable, the word 'Total' is inserted there.
*** This can be localised via: var summableTotalText = 'Whatever';

Notes:
* Takes the contents of the cell, strips everything that isn't numeric (0 to 9, plus, minus, period)
** Only concession is space, anything after a valid value and a space is ignored, eg "a 5" is 5 but "7 5" is 7.
** This version only works with period-delimited fractions currently.
*** Probably could use some weird JS-global definable formatnum rules. Meh. Sortable is horrible for i18n too.
** Doesn't support exponents or dates or any weird shit yet. Should ignore currency signs.
* Should work fine with sortable tables (needs testing).
* Uses .getElementsByTagName to find 'tr' (doublechecking depth), but .childNodes to get TD/TH
** Is that naughty?

Minimal example:

{| class="summable" border="1"
|- class="summable-row-skip"
|| Name
|class="summable-col"| Bob
|class="summable-col"| Joe
|class="summable-col"| Eve
|class="summable-col"| Ted
|- 
|| Game 1 || 5 || 7 || 0 || -1
|-
|| Game 2 || 1 || 2 || 4 || 3
|-
|| Game 3 || 7 || 2 || 6 || 2
|-
|| Game 4 || 0 || 3 || 12 || 2
|- class="summable-row-result"
|| Totals || || || ||
|}

*/

// debug
appendCSS('.summable-counted {background-color:#ffbbff;}'
 + '.summable-col {background-color:#ffffbb;}'
 + '.summable-generated-result {background-color:#bbffff;}'
)


// init tables, find them and give them IDs, and scan them for summable bits
function sumtables_init() {
  var idnum = 0;
  var tables = getElementsByClassName(document, 'table', 'summable');
    for (var i=0;i<tables.length;i++) {
      if (!tables[i].id) {
        tables[i].setAttribute('id','summable_table_id_' + idnum);
        ++idnum;
      }
    sumtables_sum(tables[i]);
  }
}
$(sumtables_init);

//Do the main addition
function sumtables_sum(table) {

  // we only want to go one level deep, so lets shift focus to the tbody
  var tbody = table.getElementsByTagName('tbody');
  if(tbody && tbody.length > 0) table = tbody[0]

  // set up fillable arrays and globals
  var tr = table.getElementsByTagName('tr');
  if(tr.length == 0) return
  var trsummable = [];
  var trresult = [];
  var trdef = tr[0];
  var cellsummable = [];
  var totaltxt = window.summableTotalText || 'Total';

  // iterate over all the rows, looking for summable, defining, skippable, and the result
  for(var i=0;i<tr.length;i++) {
    if(tr[i].parentNode != table) continue    //make sure we're only one level deep, don't grab child table elements
    var classes = ' ' + tr[i].className + ' ';
    if(classes.indexOf(' summable-row-skip ') != -1) {
      // just skip
    } else if(classes.indexOf(' summable-row-result ') != -1) {
      trresult.push(tr[i]);
    } else {
      trsummable.push(tr[i]);
    }
    // use this row for the defintions, otherwise use the first row by default
    if(classes.indexOf(' summable-row-cols ') != -1) trdef = tr[i]
  }
  if(trsummable == 0) return

  // lets look for class="summable-col", and flag these colums as summable
  var dcell = getTableCells(trdef);
  for(var i=0;i<dcell.length;i++) {
    var classes = ' ' + dcell[i].className + ' ';
    if(classes.indexOf(' summable-col ') != -1) {
      cellsummable[i] = 0;
    }
  }

  // iterate over the summable rows and find the summable columns, keep a running count
  for(var i=0;i<trsummable.length;i++) {
    var cell = getTableCells(trsummable[i]);
    for(var j=0;j<cell.length;j++) {    
      if(typeof cellsummable[j] != 'undefined') {
        cell[j].className += ' summable-counted';
        var txt = getInnerText(cell[j]);
        // normalize minus signs, and strip everything not number related
        txt = txt.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g,'-');    
        txt = txt.replace(/[^0-9+. -]/g,'');
        cellsummable[j] += parseFloat(txt);
      }
    }
  }

  // if no summable-row-result was found, just create one at the bottom
  if(trresult.length == 0) {
    var trr = document.createElement('tr');
    trr.setAttribute('class','summable-row-result sortbottom');
    trr.style.fontWeight = 'bold';
    var cell = getTableCells(trdef);
    for(var j=0;j<cell.length;j++) {    
      var td = document.createElement('td');
      // if the first cell isn't sortable, insert 'Total' literally
      if(typeof cellsummable[j] == 'undefined' && j == 0) {
        td.appendChild(document.createTextNode(totaltxt));
      }
      trr.appendChild(td);
    }
    table.appendChild(trr);
    trresult = [trr];
  }

  // generate the results
  for(var i=0;i<trresult.length;i++) {
    var cell = getTableCells(trresult[i]);
    for(var j=0;j<cell.length;j++) {    
      if(typeof cellsummable[j] != 'undefined') {
        //empty this cell and
        while(cell[j].firstChild) cell[j].removeChild(cell[j].firstChild)
        var span = document.createElement('span');
        span.setAttribute('class','summable-generated-result');
        // convert to string, and regex for parseFloat inaccuracies, parseFloat(.4+.2) => 0.6000000000000001
        var txt = '' + cellsummable[j];
        txt = txt.replace(/\.?(\d)\1{10,}\d?$/,''); 
        span.appendChild(document.createTextNode(txt));
        cell[j].appendChild(span);
      }
    }    
  }
}

// helper function, get any first generation <td> or <th>
function getTableCells(row) {
  var tdth = [];
  var cell = row.childNodes
  for(var i=0;i<cell.length;i++) {
    if(cell[i].tagName == 'TD' || cell[i].tagName == 'TH') {
      tdth.push(cell[i]);
    }
  }
  return tdth;
}