User:Tcncv/sorttables.js

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.
/*
 * Draft fix for:  https://bugzilla.wikimedia.org/show_bug.cgi?id=8028
 *
 * Current status: Shelved due to limited interest.
 *
 * Enhancements:
 *   1.  Will explode rowspans, so that rows are self contained and can be sorted
 *       without corrupting the table structure.
 *   2.  Will recognize colspans, so that the proper value is retrieved from each
 *       row.  Each column in a colspan range is treated as having the same value.
 *       Colspans are preserved during sorting; they are not split.
 *   3.  After sorting, some cell ranges may be recombined under certain restrictive
 *       conditions.  The class="autorowspan" option can be applied to column
 *       headers or the entire table to enable more aggressive rowspan combines, such
 *       as combining cells in the currently sorted column that were not originally
 *       combined.  Current merge rules:
 *         a. Only merge cells in adjacent sorted columns, selected right to left.
 *         b. Only merge if cells to left also merged, or if leftmost sorted column.
 *         c. Only merge if cells have same ID or if class="autorowspan" is active.
 *         d. And of course, cells must be equivalent (content and attributes).
 *         e. Do not merge header, footer (sortbottom) or fixed (unsortable) rows.
 *   4.  Supports multi-row headers with a mix of rowspans and colspans.  Clicking on
 *       the sort icon in a colspan'd header will perform a multi-column sort.  The
 *       "unsortable" class name can be used in the header to limit sort icon creation.
 *
 *  Bugs/Limitations:
 *   1.  Conflicting (partially overlapping) rowspans/colspans are not supported.  (This
 *       is invalid in HTML, so is not a worry.)
 *   2.  Rowspans that add extra table rows are not supported.
 *   3.  Table row attributes are not compared when combining cells.
 *   4.  Some intermittent problems observed very early in development when using table
 *       sorting + Twinkle + IE + with complex pages having large amounts of table data.
 *       (This problem has not been duplicated.)
 *
 *  Todo:
 *   1.  Restore thead/tbody/tfoot support (see bugzilla bug id 4740)
 *   2.  Merge with any recent released source changes.
 *   3.  Test on other browser configurations.
 *   4.  Solicit feature discussion and code review.
 *   5.  Prepare formal test cases and submit to bugzilla.
 *
 *  Related:
 *   1.  [[Help:Sorting]] - Document new capabilities with examples.
 *   2.  [[Wikipedia:Catalogue of CSS classes]] - Add new "autorowspan" class and
 *       update "unsortable" to document new use.
 *
 *  Other possible enhancements (extra complexity may not be justified):
 *   1.  Consider if it be useful to apply autorowspan to the original table before
 *       its initial display?  (Might same some tedious table formatting effort.)
 *   2.  Option to automatically apply initial sort.
 *   3.  Implement mixed mode sorting option (numbers, dates, and text) in a manner
 *       similar to Excel, and possibly compound sorting of mixed data (9Z < 10A).
 *   4.  Support fixed columns whose cells do not move with the sort (a lot of work).
 *
 */

/* The following is based on code extracted from wikibits
 * (/trunk/phase3/skins/common/wikibits.js) revision 45304, Fri Jan 2, 2009.
 * (http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/skins/common/wikibits.js?view=log)
 * All global variables and functions were renamed from a "ts_" prefix to
 * "tsx_" (Table Sort eXperimental) prefix.  The table class affected was
 * changed from "sortable" to "tsx_sortable".
 */


/*
 * Table sorting script based on one (c) 1997-2006 Stuart Langridge and Joost
 * de Valk:
 * http://www.joostdevalk.nl/code/sortable-table/
 * http://www.kryogenix.org/code/browser/sorttable/
 *
 * @todo don't break on colspans/rowspans (bug 8028)
 * @todo language-specific digit grouping/decimals (bug 8063)
 * @todo support all accepted date formats (bug 8226)
 */
 
var tsx_image_path = stylepath+"/common/images/";
var tsx_image_up = "sort_up.gif";
var tsx_image_down = "sort_down.gif";
var tsx_image_none = "sort_none.gif";
var tsx_europeandate = mw.config.get('wgContentLanguage') != "en"; // The non-American-inclined can change to "true"
var tsx_alternate_row_colors = false;
var tsx_number_transform_table = null;
var tsx_number_regex = null;
var tsx_SortedColumnRanges = new Array;

function tsx_sortables_init() {

	var idnum = 0;
	// Find all tables with class sortable and make them sortable
	var tables = document.querySelectorAll("table.tsx_sortable");
	for (var ti = 0; ti < tables.length ; ti++) {
		if (!tables[ti].id) {
			tables[ti].setAttribute('id','sortable_table_id_tsx_'+idnum);
			++idnum;
		}
		tsx_makeSortable(tables[ti]);
	}
}
addOnloadHook(tsx_sortables_init);

function tsx_makeSortable(table) {
	if (!table.rows || table.rows.length == 0) return;

	// Count header rows.  First row is always considered part of the header.
	// Also include rows having class=sortheader" or containing only TH cells.
	var numHeaders = 1; // Also equals rowStart
	var isHeader = true;
	for (var r = 1; r < table.rows.length && isHeader; r++) {
		if ((" "+table.rows[r].className+" ").indexOf(" sortheader ") == -1) {
			for (var i = 0; i < table.rows[r].cells.length && isHeader; i++) {
				if (table.rows[r].cells[i].nodeName.toUpperCase() != "TH") {
					isHeader = false;
				}
			}
		}
		if (isHeader) numHeaders = r + 1;
	}
	if (table.rows.length - numHeaders < 2) return;

	var repeatedCells = new Array();
	for (var r = 0; r < numHeaders; r++) {
 		var row = table.rows[r];
		var c = 0; // column number and repeatedCells index
		var i = 0; // cells index (may be less than column number)
		while (i < row.cells.length || c < repeatedCells.length) {
			if (c < repeatedCells.length && repeatedCells[c] && repeatedCells[c].remaining > 0) {
				// Use repeated cell
				repeatedCells[c].remaining--;  // remaining_repeats
				c += repeatedCells[c].cell.colSpan;
			}
			else if (i < row.cells.length ) {
				// Use existing defined cell.  If rowspan, save for later duplication.
				var cell = row.cells[i];
				if ((" "+cell.className+" ").indexOf(" unsortable ") == -1) {
					cell.innerHTML += '&nbsp;&nbsp;'
						+ '<a href="#" class="sortheader" '
						+ 'onclick="tsx_resortTable(this,'
						+ numHeaders.toString()
						+ ',' + c.toString()
						+ ',' + cell.colSpan.toString()
						+ ');return false;">'
						+ '<span class="sortarrow">'
						+ '<img src="'
						+ tsx_image_path
						+ tsx_image_none
						+ '" alt="&#x21C5;"/></span></a>';
				}
				if (cell.rowSpan > 1) {
					repeatedCells[c] = new tsx_RepeatedCell(cell);
				}
				c += cell.colSpan;
				i++;
			}
			else  {
				c += 1; //undefined cell
				i++;
			}
		}
	}

	if (tsx_alternate_row_colors) {
		tsx_alternate(table);
	}
}

function tsx_copyCell(to_cell, from_cell) {
	to_cell.innerHTML = from_cell.innerHTML;
	from_cell.innerHTML = from_cell.innerHTML;   // Copy to self - IE morphs some values
	for (var i = 0; i < from_cell.attributes.length; i++) {
		var nodeName = from_cell.attributes[i].nodeName;
		var nodeValue = from_cell.getAttribute(nodeName);
		var nodeValueType = typeof nodeValue;
		if (nodeValue!=null
			&& (nodeValueType == "string" || nodeValueType == "number" || nodeValueType == "boolean"))
		{
			to_cell.setAttribute(nodeName, nodeValue);
		}
	}
	to_cell.innerHTML = from_cell.innerHTML;    // Overkill
	from_cell.innerHTML = from_cell.innerHTML;  // Overkill
}

function tsx_compareCells(lhs, rhs) {
	if (lhs.innerHTML != rhs.innerHTML) return false;
	for (var i = 0; i < lhs.attributes.length; i++) {
		var nodeName = lhs.attributes[i].nodeName;
		var nodeNameLower = nodeName.toLowerCase();  /* IE uses mixed case */
		var nodeValue = lhs.attributes[i].nodeValue;
		var nodeValueType = typeof nodeValue;
		if (nodeNameLower != "id" && nodeNameLower != "rowspan" 
			&& nodeValue!=null
			&& (nodeValueType == "string" || nodeValueType == "number" || nodeValueType == "boolean"))
		{
			if (rhs.getAttribute(nodeName) != lhs.getAttribute(nodeName)) {
				return false;
			}
		}
	}
	for (var i = 0; i < rhs.attributes.length; i++) {
		var nodeName = rhs.attributes[i].nodeName;
		var nodeNameLower = nodeName.toLowerCase();  /* IE uses mixed case */
		var nodeValue = rhs.attributes[i].nodeValue;
		var nodeValueType = typeof nodeValue;
		if (nodeNameLower != "id" && nodeNameLower != "rowspan" 
			&& nodeValue!=null
			&& (nodeValueType == "string" || nodeValueType == "number" || nodeValueType == "boolean"))
		{
			if (rhs.getAttribute(nodeName) != lhs.getAttribute(nodeName)) {
				return false;
			}
		}
	}
	return true;
}

// Construct object to track remaining occurrences or rowspanned cell
function tsx_RepeatedCell(cell) {
	this.cell = cell;
	this.remaining = cell.rowSpan - 1;
}

// Identify and duplicate rowspanned cells so that each row has its own copy
function tsx_explodeRowspans(table, rowStart) {
	var rowspangroup_seq = 0;  // Used to generate ids for rowspan cell groups
	var repeatedCells = new Array();
	for (var r = rowStart; r < table.rows.length; r++) {
 		var row = table.rows[r];
		var c = 0; // column number and repeatedCells index
		var i = 0; // cells index (may be less than column number)
		while (i < row.cells.length || c < repeatedCells.length) {
			if (c < repeatedCells.length && repeatedCells[c] && repeatedCells[c].remaining > 0) {
				// Use repeated cell
				row.insertCell(i);
				tsx_copyCell(row.cells[i], repeatedCells[c].cell);
				row.cells[i].rowSpan = 1;
				repeatedCells[c].remaining--;  // remaining_repeats
			}
			else if (i < row.cells.length ) {
				// Use existing defined cell.  If rowspan, save for later duplication.
				if (row.cells[i].rowSpan > 1) {
					if (row.cells[i].id == "" ) {
						row.cells[i].id = table.id + ".rowspangroup." + (++rowspangroup_seq);
					}
					repeatedCells[c] = new tsx_RepeatedCell(row.cells[i]);
					row.cells[i].rowSpan = 1;
				}
			}
			else  {
				// Insert filler cell
				row.insertCell(i);
			}

			c += row.cells[i].colSpan;  // Note: Conflicting rowspan/colspan are not supported
			i++;
		}

		// Trim any trailing completed rowspans (and trailing null elements)
		while (repeatedCells.length > 0
			&& (!repeatedCells[repeatedCells.length-1] || repeatedCells[repeatedCells.length-1].remaining == 0))
				repeatedCells.length--;
	}
}

// Construct object to hold range of adjacent sorted columns
function tsx_SortedColumnRange(table, sortColumn, sortSpan) {
	this.id = table.id;
	this.from = sortColumn;
	this.thru = sortColumn + sortSpan - 1;
	this.extend = function tsx_SortedColumnRange_extend(sortColumn, sortSpan) {
		// Track columns sorted in sequence from right to left.  Reset if jump
		var sortThru = sortColumn + sortSpan - 1;
		if (sortThru  < this.from - 1 || sortThru  > this.thru ) this.thru = sortThru;
		this.from = sortColumn;
		return this;
	}
}

// Get and extend range of sorted columns
function tsx_GetSortedColumnRange(table, sortColumn, sortSpan) {
	for (var i = 0; i < tsx_SortedColumnRanges.length; i++) {
		if (table.id == tsx_SortedColumnRanges[i].id) {
			return tsx_SortedColumnRanges[i].extend(sortColumn, sortSpan);
		}
	}
	tsx_SortedColumnRanges.push(new tsx_SortedColumnRange(table, sortColumn, sortSpan));
	return tsx_SortedColumnRanges[tsx_SortedColumnRanges.length-1];
}

// Build array, indexed by column number, with a flag indicating if autorowspan is enabled
function tsx_GetAutoRowSpanColumns(table, rowStart, sortColumn, sortSpan) {
	var autoRowSpanTable = ((" "+table.className+" ").indexOf(" autorowspan ") >= 0);
	var autoRowSpanColumns = new Array();

	var repeatedCells = new Array();
	for (var r = 0; r < rowStart; r++) {
 		var row = table.rows[r];
		var c = 0; // column number and repeatedCells index
		var i = 0; // cells index (may be less than column number)
		while (i < row.cells.length || c < repeatedCells.length) {
			if (c < repeatedCells.length && repeatedCells[c] && repeatedCells[c].remaining > 0) {
				// Repeat prior rowspanned cell
				repeatedCells[c].remaining--;  // remaining_repeats
				c += repeatedCells[c].cell.colSpan;
			}
			else if (i < row.cells.length ) {
				// Use given cell
				var cell = row.cells[i];
				if ( autoRowSpanTable || (" "+cell.className+" ").indexOf(" autorowspan ") >= 0 ) {
					for (var j = 0; j < cell.colSpan; j++) {
						autoRowSpanColumns[c+j] = true;
					}
				}
				if (cell.rowSpan > 1) {
					repeatedCells[c] = new tsx_RepeatedCell(cell);
				}
				c += cell.colSpan;
				i++;
			}
			else  {
				// Skip undefined cell
				c += 1;
				i++;
			}
		}
	}

	return autoRowSpanColumns;
 }

// After sorting, scan for and combine repeated cells, where allowed
function tsx_combineRowspans(table, rowStart, sortColumn, sortSpan) {
	var SortedColumnRange = tsx_GetSortedColumnRange(table, sortColumn, sortSpan);
	var autoRowSpanColumns = tsx_GetAutoRowSpanColumns(table, rowStart, sortColumn, sortSpan);

	var priorCells = new Array();
	for (var r = rowStart; r < table.rows.length; r++) {
 		var row = table.rows[r];
		if ((" "+row.className+" ").indexOf(" unsortable ") != -1 ||
		    (" "+row.className+" ").indexOf(" sortbottom ") != -1)
		{
			priorCells.length = 0;  // Reset - Do skip and not span across fixed rows
		}
		else {
			var c = 0; // column number and priorCells index
			var i = 0; // cells index (may be less than column number)
			var merging = false;
			while (i < row.cells.length) {
				// (1) Only merge cells in adjacent sorted columns, selected right to left.
				// (2) Only merge if cells to left also merged, or if leftmost sorted column.
				// (3) Merge only if cells have same ID or class="autorowspan" is active
 				// (4) And of course, cells must be equivalent.
				if (c >= SortedColumnRange.from && c <= SortedColumnRange.thru
					&& (c == sortColumn || merging)
					&& c < priorCells.length && priorCells[c]
					&& ( (autoRowSpanColumns.length > c && autoRowSpanColumns[c])
						|| (row.cells[i].id != "" && row.cells[i].id == priorCells[c].id) )
					&& tsx_compareCells(row.cells[i],priorCells[c]) )
				{
					merging = true;
					// Match - update rowspan in prior row's tableCell and delete current.
					priorCells[c].rowSpan++;
					for (var j = 1; j < row.cells[i].colSpan; j++) priorCells[c+j] = null;  // Skipped
					c += row.cells[i].colSpan;
					row.deleteCell(i);
				}
				else {
					merging = false;
					// No match or not allowed - save, but leave unchanged.
					priorCells[c] = row.cells[i];
					for (var j = 1; j < row.cells[i].colSpan; j++) priorCells[c+j] = null;
					c += row.cells[i].colSpan;
					i++;
				}
			}
			priorCells.length = c;
		}
	}
}
 
function tsx_getInnerText(row,column) {
	var i = 0;
	var c = 0;
	var ncells = row.cells.length;
	while (i < ncells && c <= column) {
		if (column >= c && column < c + row.cells[i].colSpan) {
			return getInnerText( row.cells[i] );
		}
		c += row.cells[i].colSpan;
		i++;
	}
	return "";
}
 
function tsx_resortTable(lnk, rowStart, sortColumn, sortSpan) {
	// Get the span containing the sort icon and determine sort direction
	var span = lnk.getElementsByTagName('span')[0];
 	var reverse = (span.getAttribute("sortdir") == 'down');

	// Get the table
	var td = lnk.parentNode;
	var tr = td.parentNode;
	var table = tr.parentNode;
	while (table && !(table.tagName && table.tagName.toLowerCase() == 'table'))
		table = table.parentNode;
	if (!table) return;
 
	// Generate the number transform table if it's not done already
	if (tsx_number_transform_table == null) {
		tsx_initTransformTable();
	}

	// Expand any rowspan'ed cells that could potentially be split by sort
	tsx_explodeRowspans(table,rowStart);

	// Sort each column in the selected range from right to left
	for (var i = sortSpan - 1; i >= 0 ; i--) {
		tsx_sortColumn(table, rowStart, sortColumn + i, reverse);
	}

 	// Merge cells into rowspans, where possible
	tsx_combineRowspans(table, rowStart, sortColumn, sortSpan);

	var arrowHTML;
	if (reverse) {
		arrowHTML = '<img src="'+ tsx_image_path + tsx_image_down + '" alt="&darr;"/>';
		span.setAttribute('sortdir','up');
	} else {
		arrowHTML = '<img src="'+ tsx_image_path + tsx_image_up + '" alt="&uarr;"/>';
		span.setAttribute('sortdir','down');
	}

	// Delete any other arrows there may be showing
	for (var r = 0; r < rowStart; r++) {
		var spans = table.rows[r].querySelectorAll("span.sortarrow");
		for (var i = 0; i < spans.length; i++) {
			spans[i].innerHTML = '<img src="'+ tsx_image_path + tsx_image_none + '" alt="&#x21C5;"/>';
		}
	}
	span.innerHTML = arrowHTML;
 
	if (tsx_alternate_row_colors) {
		tsx_alternate(table);
	}
}

function tsx_sortColumn(table, rowStart, column, reverse) {
	// Work out a type for the column
	var itm = "";
	for (var i = rowStart; i < table.rows.length; i++) {
		if (table.rows[i].cells.length > column) {
			itm = tsx_getInnerText(table.rows[i],column);
			itm = itm.replace(/^[\s\xa0]+/, "").replace(/[\s\xa0]+$/, "");
			if (itm != "") break;
		}
	}

	// TODO: bug 8226, localised date formats
	var sortfn = tsx_sort_generic;
	var preprocessor = tsx_toLowerCase;
	if (/^\d\d[\/. -][a-zA-Z]{3}[\/. -]\d\d\d\d$/.test(itm)) {
		preprocessor = tsx_dateToSortKey;
	} else if (/^\d\d[\/.-]\d\d[\/.-]\d\d\d\d$/.test(itm)) {
		preprocessor = tsx_dateToSortKey;
	} else if (/^\d\d[\/.-]\d\d[\/.-]\d\d$/.test(itm)) {
		preprocessor = tsx_dateToSortKey;
	// pound dollar euro yen currency cents
	} else if (/(^[\u00a3$\u20ac\u00a4\u00a5]|\u00a2$)/.test(itm)) {
		preprocessor = tsx_currencyToSortKey;
	} else if (tsx_number_regex.test(itm)) {
		preprocessor = tsx_parseFloat;
	}
 
	var newRows = new Array();
	var staticRows = new Array();
	for (var j = rowStart; j < table.rows.length; j++) {
		var row = table.rows[j];
		if((" "+row.className+" ").indexOf(" unsortable ") < 0) {
			var keyText = tsx_getInnerText(row,column);
			var oldIndex = (reverse ? -j : j);
			var preprocessed = preprocessor( keyText );
 
			newRows[newRows.length] = new Array(row, preprocessed, oldIndex);
		} else staticRows[staticRows.length] = new Array(row, false, j-rowStart);
	}
 
	newRows.sort(sortfn);
	if (reverse) newRows.reverse();

	for (var i = 0; i < staticRows.length; i++) {
		var row = staticRows[i];
		newRows.splice(row[2], 0, row);
	}

	// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
	// don't do sortbottom rows
	for (var i = 0; i < newRows.length; i++) {
		if ((" "+newRows[i][0].className+" ").indexOf(" sortbottom ") == -1)
			table.tBodies[0].appendChild(newRows[i][0]);
	}

	// do sortbottom rows only
	for (var i = 0; i < newRows.length; i++) {
		if ((" "+newRows[i][0].className+" ").indexOf(" sortbottom ") != -1)
			table.tBodies[0].appendChild(newRows[i][0]);
	}
}

function tsx_initTransformTable() {
	if ( typeof mw.config.get('wgSeparatorTransformTable') == "undefined"
			|| ( mw.config.get('wgSeparatorTransformTable')[0] == '' && mw.config.get('wgDigitTransformTable')[2] == '' ) )
	{
		digitClass = "[0-9,.]";
		tsx_number_transform_table = false;
	} else {
		tsx_number_transform_table = {};
		// Unpack the transform table
		// Separators
		ascii = mw.config.get('wgSeparatorTransformTable')[0].split("\t");
		localised = mw.config.get('wgSeparatorTransformTable')[1].split("\t");
		for ( var i = 0; i < ascii.length; i++ ) { 
			tsx_number_transform_table[localised[i]] = ascii[i];
		}
		// Digits
		ascii = mw.config.get('wgDigitTransformTable')[0].split("\t");
		localised = mw.config.get('wgDigitTransformTable')[1].split("\t");
		for ( var i = 0; i < ascii.length; i++ ) { 
			tsx_number_transform_table[localised[i]] = ascii[i];
		}
 
		// Construct regex for number identification
		digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '\\.'];
		maxDigitLength = 1;
		for ( var digit in tsx_number_transform_table ) {
			// Escape regex metacharacters
			digits.push( 
				digit.replace( /[\\\\$\*\+\?\.\(\)\|\{\}\[\]\-]/,
					function( s ) { return '\\' + s; } )
			);
			if (digit.length > maxDigitLength) {
				maxDigitLength = digit.length;
			}
		}
		if ( maxDigitLength > 1 ) {
			digitClass = '[' + digits.join( '', digits ) + ']';
		} else {
			digitClass = '(' + digits.join( '|', digits ) + ')';
		}
	}
 
	// We allow a trailing percent sign, which we just strip.  This works fine
	// if percents and regular numbers aren't being mixed.
	tsx_number_regex = new RegExp(
		"^(" +
			"[+-]?[0-9][0-9,]*(\\.[0-9,]*)?(E[+-]?[0-9][0-9,]*)?" + // Fortran-style scientific
			"|" +
			"[+-]?" + digitClass + "+%?" + // Generic localised
		")$", "i"
	);
}
 
function tsx_toLowerCase( s ) {
	return s.toLowerCase();
}
 
function tsx_dateToSortKey(date) {	
	// y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
	if (date.length == 11) {
		switch (date.substr(3,3).toLowerCase()) {
			case "jan": var month = "01"; break;
			case "feb": var month = "02"; break;
			case "mar": var month = "03"; break;
			case "apr": var month = "04"; break;
			case "may": var month = "05"; break;
			case "jun": var month = "06"; break;
			case "jul": var month = "07"; break;
			case "aug": var month = "08"; break;
			case "sep": var month = "09"; break;
			case "oct": var month = "10"; break;
			case "nov": var month = "11"; break;
			case "dec": var month = "12"; break;
			// default: var month = "00";
		}
		return date.substr(7,4)+month+date.substr(0,2);
	} else if (date.length == 10) {
		if (tsx_europeandate == false) {
			return date.substr(6,4)+date.substr(0,2)+date.substr(3,2);
		} else {
			return date.substr(6,4)+date.substr(3,2)+date.substr(0,2);
		}
	} else if (date.length == 8) {
		yr = date.substr(6,2);
		if (parseInt(yr) < 50) { 
			yr = '20'+yr; 
		} else { 
			yr = '19'+yr; 
		}
		if (tsx_europeandate == true) {
			return yr+date.substr(3,2)+date.substr(0,2);
		} else {
			return yr+date.substr(0,2)+date.substr(3,2);
		}
	}
	return "00000000";
}
 
function tsx_parseFloat( s ) {
	if ( !s ) {
		return 0;
	}
	if (tsx_number_transform_table != false) {
		var newNum = '', c;
 
		for ( var p = 0; p < s.length; p++ ) {
			c = s.charAt( p );
			if (c in tsx_number_transform_table) {
				newNum += tsx_number_transform_table[c];
			} else {
				newNum += c;
			}
		}
		s = newNum;
	}
 
	num = parseFloat(s.replace(/,/g, ""));
	return (isNaN(num) ? s : num);
}
 
function tsx_currencyToSortKey( s ) {
	return tsx_parseFloat(s.replace(/[^0-9.,]/g,''));
}
 
function tsx_sort_generic(a, b) {
	return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : a[2] - b[2];
}
 
function tsx_alternate(table) {
	// Take object table and get all it's tbodies.
	var tableBodies = table.getElementsByTagName("tbody");
	// Loop through these tbodies
	for (var i = 0; i < tableBodies.length; i++) {
		// Take the tbody, and get all it's rows
		var tableRows = tableBodies[i].getElementsByTagName("tr");
		// Loop through these rows
		// Start at 1 because we want to leave the heading row untouched
		for (var j = 0; j < tableRows.length; j++) {
			// Check if j is even, and apply classes for both possible results
			var oldClasses = tableRows[j].className.split(" ");
			var newClassName = "";
			for (var k = 0; k < oldClasses.length; k++) {
				if (oldClasses[k] != "" && oldClasses[k] != "even" && oldClasses[k] != "odd")
					newClassName += oldClasses[k] + " ";
			}
			tableRows[j].className = newClassName + (j % 2 == 0 ? "even" : "odd");
		}
	}
}
 
/*
 * End of table sorting code
 */
Retrieved from "https://en.wikipedia.org/w/index.php?title=User:Tcncv/sorttables.js&oldid=1004804073"