Date: | August 19, 2013 / year-entry #221 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20130819-00/?p=3473 |
Comments: | 8 |
Summary: | 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... |
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 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
One handy thing about the functions in the
First, the
I took a few shortcuts here.
Depending on your browser, you may need to use
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 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.
JavaScript's
Similarly,
JavaScript's 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 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 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:
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. |
Comments (8)
Comments are closed. |
Perhaps the biggest difference is that LINQ is lazy, in that the results are only evaluated when you ask for them (so if you change the underlying array, you get different results back), while your methods are eager ("strict"), in that the results are computed when your functions are called.
It looks like $linq is lazy, too, as evidenced by the existence of its .toArray() function.
A few years ago, I started on writing a JavaScript-LINQ (complete with enumerables) of my own, I'm glad to see that somebody else had the same idea and wasn't as procrastinating as me! ;)
Exemplo muito show, sonho de consumo linq no JS
This decorate-sort-undecorate pattern is called a Schwartzian transform :)
One minor nitpick: Perl is the language and perl the interpreter.
Human sacrifice, dogs and cats living together, mass hysteria, Raymond Chen blogging about JavaScript.
LINQ stands for Language Integrated Query, integration meaning that the query expressions are rewritten into method invocations, particularly helpfully with "let" as you say. No such thing happens here. Maybe the proper name for this post is that you can write and use LINQ operators in JavaScript and roughly mirror the fluent enumerable API? Couldn't you call Common LISP (a great oxymoron) and Perl LINQ-compatible by this standard?
Huge nitpick, I know, but also motivated. If there's no integration, why not also write a ray tracer and call it a Language Integrated Ray Tracer?
Fair enough.