I had a little side project that displayed status information in a table,
and I figured,
hey,
let me add sorting.
And it was a lot easier than I thought.
I just put the header row in the THEAD and the table
contents in the TBODY,
then I could use this code to sort the table:
function sortByColumn(table, sortCol, direction) {
direction = direction || 1; // default sort ascending
var tBody = table.tBodies[0];
Array.prototype.map.call(tBody.rows, function (row) {
var cell = row.cells[sortCol];
return { row: row, key: cell.sortKey || cell.innerText };
}).sort(function (a, b) {
if (a.key < b.key) return -direction;
if (a.key > b.key) return direction;
return 0;
}).forEach(function (o) {
tBody.appendChild(o.row);
});
}
Each cell can have an optional sortKey custom attribute
which specifies how the item should sort.
If there is no sortKey, then I just use the cell’s
innerText.
(My table was constructed at runtime from an
XmlHttpRequest,
so adding the sortKey to the date fields was not difficult.)
One handy thing about the functions in the Array prototype
is that as a rule, they do not actually require that the this
object be an array.
As long as it has a length property and integer subscripts,
you can use it as if it were an array.
The map function is okay with read-only access;
some other function like sort require read-write access.
To call a function with a custom this parameter,
you use the call method on the function object itself,
passing the artificial this as the first parameter,
with the remaining parameters following.
First, the sortByColumn
function takes the rows of the table body and
maps each one to a record consisting of the sort key
and the original row.
The sort key is the sortKey property,
if true-ish, we will use it; otherwise, we use the text of the cell.
I took a few shortcuts here.
Depending on your browser, you may need to use
textContent instead of innerText,
and you may need to use getAttribute instead of
property notation.
And my function doesn’t handle the case where the sort key is defined
but is false-ish.
Here’s a more general version:
var textProperty = table.innerText ? "innerText" : "textContent";
...
return { row: row,
key: cell.hasAttribute("sortKey") ?
cell.getAttribute(sortKey") :
cell[textProperty] };
...
Anyway, after we map the rows to an array of sort records,
we sort the records by comparing the key,
either by string or by number.
The code assumes that every column is either all-strings or all-numbers;
it doesn’t try to handle the mixed case.
This is easy to enforce in the code
that generates the table
because the only way to get a non-string
as a sort key is to set it explicitly as the sortKey
attribute.
Finally, we take the sorted records and insert the sorted rows back into the table.
This is a common programming pattern: Decorate, operate, undecorate.¹ We started with a bunch of rows, and we wanted to sort them. We can’t sort rows directly, so instead we converted the rows into something we can sort, but remembered the row that each converted item came from. We then perform the sort operation, and then recover the original rows from the decoration, now in sorted order, which we can then use for whatever operation we really wanted. I sort of combined the last two step into one. More formally, it would look like this:
function sortByColumn(table, sortCol, direction) {
direction = direction || 1; // default sort ascending
var tBody = table.tBodies[0];
// decorate: convert the row into a record
Array.prototype.map.call(tBody.rows, function (row) {
var cell = row.cells[sortCol];
return { row: row, key: cell.sortKey || cell.innerText };
})
// operate on the record
.sort(function (a, b) {
if (a.key < b.key) return -direction;
if (a.key > b.key) return direction;
return 0;
})
// undecorate: convert the record back into a row
.map(function (o) {
return o.row;
})
// operate on the sorted rows
.forEach(function (r) {
tBody.appendChild(r);
});
}
Category theorists I’m sure have some fancy names they can use to describe this concept, like natural transformation and functor category and splitting.
LINQ also has a fancy name for this: let, which is a special case of select where LINQ generates the record for you.
| LINQ let query | from d in data let y = f(d.xValue) |
|---|---|
| LINQ query | from d in data
select new { d = d, y = f(d.xValue) } |
| LINQ fluent | data.Select(d =>
new { d = d, y = f(d.xValue) }) |
| LINQ fluent old delegate syntax |
data.Select(delegate(Data d) {
return new { d = d, y = f(d.xValue) }; }) |
| JavaScript | data.map(function (d)
{ return { d: d, y: f(d.xValue) }; }) |
JavaScript’s map is the same as LINQ’s Select,
just with different decorative bits.
data.Select(delegate(Data d)
{ return new { d = d, y = f(d.xValue) }; }) |
data.map (function( d)
{ return
{ d: d, y: f(d.xValue) }; }) |
Similarly,
JavaScript’s filter is the same as LINQ’s Where,
JavaScript’s some is the same as LINQ’s Any,
JavaScript’s every is the same as LINQ’s All,
and
JavaScript’s reduce is the same as LINQ’s Aggregate.
JavaScript’s sort is sort of like LINQ’s Sort,
except that it modifies the array in place rather than generating a new
result.
Bonus chatter: In theory, I could’ve just sorted the table directly by doing the sort key extraction inside the comparator:
function sortByColumn(table, sortCol, direction) {
direction = direction || 1; // default sort ascending
var tBody = table.tBodies[0];
Array.prototype.map.call(tBody.rows, function (r) {
return r;
}).sort(function (a, b) {
var keyA = a.cells[sortCol].sortKey || a.cells[sortCol].innerText;
var keyB = b.cells[sortCol].sortKey || b.cells[sortCol].innerText;
if (keyA < keyB) return -direction;
if (keyA > keyB) return direction;
return 0;
}).forEach(function (r) {
tBody.appendChild(r);
});
}
but since I had to convert the rows into an array anyway
(since you cannot modify the rows property by subscript
assignment), I figured I’d do the extracting while I was there.
I guess I could’ve added a LINQy sort method:
function defaultComparator(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
Array.prototype.orderBy =
function Array_orderBy(extractKey, comparator, direction) {
direction = direction || 1;
comparator = comparator || defaultComparator;
return Array.prototype.map.call(this, function (d) {
return { key: extractKey.call(d), original: d };
}).sort(function (a, b) {
return direction * comparator(a.key, b.key);
}).map(function (r) {
return r.original;
});
};
Then my sortByColumn function would just be
function sortByColumn(table, sortCol, direction) {
direction = direction || 1; // default sort ascending
var tBody = table.tBodies[0];
Array.prototype.orderBy.call(tBody.rows, function (r) {
var cell = r.cells[sortCol];
return { key: cell.sortKey || cell.innerText, row: r };
}, direction).forEach(function (r) {
tBody.appendChild(r);
});
}
But if I had done that, I wouldn’t have had a cute one-function table sorter!
¹ In perl, this pattern is known as the Schwartzian transform. I prefer to think of it as completing the commutative diagram:
| g | ||||
| B | → | B | ||
| f | ↓ | ↓ | f | |
| A | ⇢ | A | ||
| f ∘ g ∘ f⁻¹ | ||||
Mathematicians get all excited when they see something of the form f ∘ g ∘ f⁻¹: That’s the form of a conjugation operation. Which makes sense, because conjugation is a way of looking at an algebraic group through different-colored glasses. In our case, the magic glasses make every row look like its sort key.
Bonus chatter: $linq is a Javascript LINQ library.
0 comments