The tiny table sorter – or – you can write LINQ in JavaScript

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 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 sort­Key custom attribute which specifies how the item should sort. If there is no sort­Key, then I just use the cell's inner­Text. (My table was constructed at runtime from an Xml­Http­Request, so adding the sort­Key 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 sort­By­Column 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 sort­Key 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 text­Content instead of inner­Text, and you may need to use get­Attribute 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 sort­Key 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 sort­By­Column 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
fgf⁻¹

Mathematicians get all excited when they see something of the form fgf⁻¹: 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)
  1. Gabe says:

    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.

  2. Miff says:

    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! ;)

  3. Exemplo muito show, sonho de consumo linq no JS

  4. The Barnyard says:

    This decorate-sort-undecorate pattern is called a Schwartzian transform :)

    [Is there anything else in the article you feel like repeating? -Raymond]
  5. One minor nitpick: Perl is the language and perl the interpreter.

  6. John Styles says:

    Human sacrifice, dogs and cats living together, mass hysteria, Raymond Chen blogging about JavaScript.

  7. 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?

    [Because "You can write something that syntactically resembles LINQ in JavaScript" is more awkward. -Raymond]

Comments are closed.


*DISCLAIMER: I DO NOT OWN THIS CONTENT. If you are the owner and would like it removed, please contact me. The content herein is an archived reproduction of entries from Raymond Chen's "Old New Thing" Blog (most recent link is here). It may have slight formatting modifications for consistency and to improve readability.

WHY DID I DUPLICATE THIS CONTENT HERE? Let me first say this site has never had anything to sell and has never shown ads of any kind. I have nothing monetarily to gain by duplicating content here. Because I had made my own local copy of this content throughout the years, for ease of using tools like grep, I decided to put it online after I discovered some of the original content previously and publicly available, had disappeared approximately early to mid 2019. At the same time, I present the content in an easily accessible theme-agnostic way.

The information provided by Raymond's blog is, for all practical purposes, more authoritative on Windows Development than Microsoft's own MSDN documentation and should be considered supplemental reading to that documentation. The wealth of missing details provided by this blog that Microsoft could not or did not document about Windows over the years is vital enough, many would agree an online "backup" of these details is a necessary endeavor. Specifics include:

<-- Back to Old New Thing Archive Index