User:Tcncv/sorttables.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Tcncv/sorttables. |
/*
* 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 = ' '
'<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="⇅"/></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="↓"/>';
span.setAttribute('sortdir','up');
} else {
arrowHTML = '<img src="' tsx_image_path tsx_image_up '" alt="↑"/>';
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="⇅"/>';
}
}
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
*/