Sorting Values with JavaScript

Lists and tables are often the best way to display data on the web; but you shouldn’t have to worry about sorting that information manually. In today’s tutorial, you’re going to make a jQuery plugin that will put all your ducks in a row with JavaScript ease!


Preface

So, how exactly does sorting work in JavaScript? It’s not too complicated: any array object has a sort method. If you don’t pass it any parameters, it will convert the objects in the array to strings, sort them pseudo-alphabetically, and return them. Usually, this is terrible; consider sorting the numbers 0 – 10 alphabetically. You would get this: [0, 1, 10, 2, 3, 4, 5, 6, 7, 8, 9]. Fortunately, we can pass a function to the sort method. That function should take two parameters (the two items to be compared): then, it will return 0 if they are equal, a negative number if the first parameter takes precedence, or a positive number of the second parameter should come first. So numbers are actually the simplest thing to sort “manually”:

1

2
numberArray.sort(function(a, b) {
3
    return a - b
4
});

Obviously, this will return 0 if the numbers are equal, a negative number if a should be first, and a positive number if b should be first.

We’re going to look at sorting several different types of data, a few in multiple formats; but this will all be much more useful if we wrap it in a jQuery plugin, so let’s start by setting up that shell!

The Plugin Shell

If you’re not familiar with writing jQuery plugins, check out Jeffrey Way’s Screencast “You still can’t create a jQuery Plugin?” It’ll get you up to speed in no time if you’re comfortable with jQuery! (true confession: I’d actually never written a plugin until I made this one).

We’ll set up our plugin, called datasort, this way: we’ll pass it an array of items to sort; we can specify four parameters.

  • datatype (the type of data you’re sorting)
  • sortElement (the child element you want to sort by, if desired)
  • sortAttr (the attribute you want to sort by, if desired)
  • reverse (the direction they should sort in)

So a fully-modified call to our plugin might look like this:

1

2
$('ul.names li).datasort({
3
    		datatype    : 'alpha',
4
    		sortElement : 'span.first',
5
    		sortAttr    : 'rel',
6
    		reverse     : true
7
    	});

Here’s the plugin shell:

1

2
(function ($) {
3
  $.fn.datasort = function(options) {
4
    var defaults = {
5
    	//set the default parameter values
6
          datatype    : 'alpha',
7
          sortElement : false,
8
          sortAttr    : false,
9
          reverse     : false
10
          },
11
    // combine the default and user's parameters, overriding defaults
12
        settings = $.extend({}, defaults, options), 
13
        datatypes = {},
14
        base = {},
15
        that = this;
16

17
    if (typeof settings.datatype === 'string') {
18
      that.sort(datatypes[settings.datatype]);
19
    }
20
    if (typeof settings.datatype === 'function') {
21
      that.sort(settings.datatype);
22
    }
23
    if(settings.reverse) {
24
      that = $($.makeArray(this).reverse());
25
    }
26
    $.each(that, function(index, element) { that.parent().append(element); });
27
  };
28
})(jQuery);

So here’s how it’ll work: we’ll set up all the variables at the beginning. Then, if the datatype parameter is a string, we’ll find the corresponding sort function in the datatypes object and sort with it; if the datatype parameter is a function, we’ll sort with it. Finally, if the reverse setting is set to true, we’ll reverse the order of the sorted items (since jQuery objects aren’t true JavaScript arrays, the reverse function won’t work on them; so we can use $.makeArray() to turn it into one; then, once it’s reversed, we re-jquery-fy it!).

Laying a Bit More Groundwork

At the very lowest level, you can sort almost any type of data in one of two ways: we’ll be calling them alphabetically and numerically. Let’s create these two functions as properties of your base object.

1

2
base = {
3
  alpha : function(a, b) {
4
    a = a.toUpperCase();
5
    b = b.toUpperCase();
6
    return (a < b) ? -1 : (a > b) : 1 : 0;
7
    //ternary operator: condition ? returnIfTrue : returnIfFalse
8
  },
9
  number : function(a, b) {
10
    a = parseFloat(a);
11
    b = parseFloat(b);
12
    return a - b;
13
  }
14
},

Pretty simple, eh? Simply normalize the two values, compare and return. The tricky part is parsing the data that we want to send to these functions; that’s what we’ll do now. However, there’s one more thing.

When sorting items in the array, we might not want to sort simply by the text of the element itself. The sortElement and sortAttr parameters of our plugin are to this end. For example, we will likely want to sort table rows based on a certain column of table cells. In that case, we’d use $(‘table tr’).datasort({ sortElement : ‘td.price’ }). Or perhaps we want to sort a list of images by their alt attributes: $(‘ul li’).datasort({sortElement : ‘img’, sortAttr : ‘alt’}). Because of all this, we need to add one more function to our base object:

1

2
base = {
3
  alpha : function (a, b) { ... },
4
  number : function (a, b) { ... },
5
  extract : function (a, b) {
6
  	var get = function (i) {
7
      var o = $(i);
8
      if (settings.sortElement) {
9
        o = o.children(settings.sortElement);
10
      }
11
      if (settings.sortAttr) {
12
        o = o.attr(settings.sortAttr);
13
      } else {
14
        o = o.text();
15
      }
16
      return o;
17
    };
18
    return {
19
      a : get(a),
20
      b : get(b)
21
    };
22
  }		
23
},

It may look complicated, but it’s not. We just create a jQuery object with each item; if sortElement is set, we use the children() method to get the right elements. Then, if a sortAttr is set, we get its value; if not, we get the element’s text. We’ve set all this to an inner function, and return an object with two properites; these properties are the values we must parse and send to the appropriate base sorting function.

This probably seemed like a lot of prep work, but what we were really doing is abstracting as much code as possible. This way, they’ll be much less repeat code, because the important actions have been bundled away as functions.

Sorting Words and Numbers

We’re finally here: the fun part! We’ll start by building two simple functions for our datatypes object. These will simple pass values to base.extract() and then pass those return values to the appropriate sorting class.

1

2
datatypes = {
3
  alpha : function (a, b) {
4
    var o = base.extract(a, b);
5
    return base.alpha(o.a, o.b);
6
  },
7
  number : function(a, b) {
8
    var o = base.extract(a, b);
9
    for (var e in o) {
10
      o[e] = o[e].replace(/[$]?(-?d+.?d+)/, '$1');
11
    }
12
    return base.number(o.a, o.b);
13
  },
14
},

Our alphabetic sorter should be obvious. The number sorter does a bit more: before passing the extracted values on, it strips out a dollar sign at the front. I’ve kept this regular expression simple, but you could parse a lot of different number formats here if you wanted to get complex. Let’s give our evolving plugin a try; create a basic html page:

1

2
<!DOCTYPE html>
3
<html>
4
<head>
5
  <meta charset='utf-8' />
6
  <title>Data Sorting</title>
7
  <style type='text/css'>
8
  ul, table {
9
    display:table;
10
    float:left;
11
    background:#ececec;
12
    margin:10px;
13
    padding:0;
14
    border:1px solid #ccc;
15
  }
16
  li, tr {
17
    margin:0;
18
    padding:8px;
19
    border-top:1px solid #fff;
20
    border-bottom:1px solid #ccc;
21
    list-style-type:none;
22
  }
23
  li:first-child { border-top:0 }
24
  li:last-child { border-bottom:0 }
25
  </style>
26
</head>
27
<body>
28
  <table class='a'>
29
    <thead>
30
      <tr>
31
        <th rel='alpha' class='first'>First Name</th>
32
        <th rel='alpha' class='last'>Last Name</th>
33
      </tr>
34
    </thead>
35
    <tbody>
36
      <tr><td class="first">Jeffrey</td> <td class="last">Way</td></tr>
37
      <tr><td class="first">Sean</td> <td class="last">Hodge</td></tr>
38
      <tr><td class="first">Adam</td> <td class="last">Miller</td></tr>
39
      <tr><td class="first">Ian</td> <td class="last">Yates</td></tr>
40
      <tr><td class="first">Adrian</td> <td class="last">Try</td></tr>
41
      <tr><td class="first">Caleb</td> <td class="last">Aylsworth</td></tr>
42
    </tbody>
43
  </table>
44

45
  <ul class='n'>
46
  <li>4.09</li>
47
  <li>4.10</li>
48
  <li>67.8</li>
49
  <li>100</li>
50
  <li>-98</li>
51
  <li>67.7</li>
52
  <li>23</li>
53
  </ul> 
54

55
  <ul class="curr">
56
    <li>$299.66</li>
57
    <li>$299.57</li>
58
    <li>$0.14</li>
59
    <li>$80.00</li>
60
  </ul>
61

62
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" /></script>
63
  <script src="jquery.datasort.js" /></script>
64
  <script type="text/javascript">
65
    $('table.a tbody tr').datasort({sortElement : 'td.last'});
66
    $('ul.n li').datasort({datatype: 'number', reverse: true});
67
    $('ul.curr li').datasort({ datatype: 'number' });
68
  </script>
69
</body>
70
</html>

I’ve included a table and two lists (and I’ve styled them briefly). Take note of our plugin calls: we’re using the default datatype for the table, but sorting by the table cells with a class of last; try changing this to ‘td.first.’ Then, we sort the lists numerically, and reverse one of them. Here’s the proof of our labours:

Pretty nice, but those were relatively simple values; what if we want to be able to sort multiple formats for one type?

Sorting Dates

There are a number of different ways to write dates, which makes it pretty tricky to parse them for sorting. However, we can cover most of them with this:

1

2
date : function(a, b) {
3
  var o = base.extract(a, b);
4
  for (var e in o) {
5
  o[e] = o[e].replace(/-/g, '')
6
             .replace(/january|jan/i, '01')
7
             .replace(/february|feb/i, '02')
8
             .replace(/march|mar/i, '03')
9
             .replace(/april|apr/i, '04')
10
             .replace(/may/i, '05')
11
             .replace(/june|jun/i, '06')
12
             .replace(/july|jul/i, '07')
13
             .replace(/august|aug/i, '08')
14
             .replace(/september|sept|sep/i, '09')
15
             .replace(/october|oct/i, '10')
16
             .replace(/november|nov/i, '11')
17
             .replace(/december|dec/i, '12')
18
             .replace(/(d{2}) (d{2}), (d{4})/, '$3$1$2')
19
             .replace(/(d{2})/(d{2})/(d{4})/, '$3$2$1');
20
  }
21
  return base.number(o.a, o.b);
22
},

So what are we doing here? First, here’s the logic: if all the dates are formatted YYYYMMDD, they will sort correctly with numerical sorting. Our parser can sort the following date formats:

  • YYYY-MM-DD
  • YYYYMMDD
  • DD/MM/YYYY
  • month DD, YYYY

First we strip our dashes, which will leave YYYY-MM-DD ready for parsing. Then, we replace every month name or abbreviation with its number value. Finally, we have to rearrange the numbers for DD/MM/YYY and month DD, YYYY. That’s what the last two expressions do. To give this a try, paste this list into our HTML:

1

2
<ul class='date'>
3
  <li>2009-10-06</li>
4
  <li>sept 25, 1995</li>
5
  <li>1990-06-18</li>
6
  <li>20100131</li>
7
  <li>June 18, 2009</li>
8
  <li>02/11/1993</li>
9
  <li>15941219</li>
10
  <li>1965-08-05</li>
11
  <li>1425-12-25</li>
12
</ul>

And call it with this:

1

2
    $('ul.date li').datasort({datatype: 'date'});

Is this a perfect date parser? Not by any means; we can’t sort DD/MM/YY, because there’s no way to know what century this is in. Also, we can’t tell the difference between DD/MM/YY and MM/DD/YY, so we just have to choose one.

Sorting Time

Sorting time values must be one of the most difficult values to sort: we need to be able to accept 12-hour time, 24-hour time, and values with or without AM/PM tags and seconds. I think it’s easiest to sort time alphabetically, even though its all numbers. Why? Consider these two timestamps: 00:15:37 and 12:15. The first one should come first, but if we sort them by number they’ll be parsed as floats, and end up like 1537 and 1215. Now, the second value will come first. Also, when sorting alphabetically, we don’t have to take out the colons (parseFloat() would choke on them). So here’s how it’s done.

1

2
time : function(a, b) {
3
  var o = base.extract(a, b),
4
      afternoon = /^(.+) PM$/i;
5
  for (var e in o) {
6
    o[e] = o[e].split(':');
7
    var last = o[e].length - 1;
8

9
    if(afternoon.test(o[e][last])) {
10
      o[e][0] = (parseInt(o[e][0]) + 12).toString();
11
      o[e][last] = o[e][last].replace(afternoon, '$1');
12
    }
13
    if(parseInt(o[e][0]) < 10 && o[e][0].length === 1) {
14
      o[e][0] = '0' + o[e][0];
15
    }
16
    o[e][last] = o[e][last].replace(/^(.+) AM$/i, '$1');
17

18
    o[e] = o[e].join('');
19
  }
20
  return base.alpha(o.a, o.b);
21
}

Let’s go through this line by line.

1

2
  var o = base.extract(a, b),
3
      afternoon = /^(.+) PM$/i;

We start with our variables: our extracted values and a regular expression to check for PM label.

1

2
  for (var e in o) {
3
    o[e] = o[e].split(':');
4
    var last = o[e].length - 1;
5

6
    if(afternoon.test(o[e][last])) {
7
      o[e][0] = (parseInt(o[e][0]) + 12).toString();
8
      o[e][last] = o[e][last].replace(afternoon, '$1');
9
    }

Next, we’ll start a for loop, going through each of the values we’re sorting; first, we split it into an array at the colons. We create an easy way to get to the last items of the array: our ‘last’ variable. Then, we test our PM regex on the last item in our array; if it returns true, this value has the PM tag. Therefore, we’ll add 12 to the first item in our array, which will be the hour value; we do this because we need all the values to be formatted in 24-hour time. (Note that to do this, we must convert it to a number, add 12, and then turn it back into a string). Finally, we use the PM regex again to remove that label from the last item in the array.

1

2
    if(parseInt(o[e][0]) < 10 && o[e][0].length === 1) {
3
      o[e][0] = '0' + o[e][0];
4
    }
5
   o[e][last] = o[e][last].replace(/^(.+) AM$/i, '$1');
6

7
    o[e] = o[e].join('');
8
}
9
return base.alpha(o.a, o.b);

In this last chunk, we check the hour value for two conditions: is it less than 10? and does the string have only one character? This is important because a value like 08 will parse as 8 and be less than 10; but we’re trying to see if we need to add a zero to the front. If the string has only one character, then we add the zero, so 3 becomes 03. This will keep things in order!

Before joining the array, we remove any AM labels. So now this . . .

1

2
<ul class='time'>
3
  <li>1:15:47</li>
4
  <li>3:45 PM</li>
5
  <li>12:00:17</li>
6
  <li>06:56</li>
7
  <li>19:39</li>
8
  <li>4:32 AM</li>
9
  <li>00:15:36</li>
10
</ul>

. . . can be sorted with this . . .

1

2
$('ul.time li').datasort({datatype: 'time'});

And we’re done! Behold the fruits of our labour:

Sorting Time

More Random Values

We’ve set up our jQuery plugin so that users can pass sorting functions as the datatype parameter. This allows us to easily extend the plugin, although we don’t have access to the base ‘class’ from the plugin call. We can easily write a function to sort psudeo-ratings:

1

2
$('ul.rating li').datasort({datatype: function(a, b) {
3
      var o  = {
4
      a : $(a).text(),
5
      b : $(b).text() 
6
      }
7
      for (var e in o) {
8
        o[e] = o[e].replace(/poor/i, 0)
9
                   .replace(/satisfactory/i, 1)
10
                   .replace(/good/i, 2)
11
                   .replace(/excellent/i, 3);
12
      }
13
      return o.a - o.b;
14
    }
15
});

This uses the simplest regular expressions possible to sort a list like this:

1

2
<ul class="rating">
3
  <li>Good</li>
4
  <li>Excellent</li>
5
  <li>Poor</li>
6
  <li>Satisfactory</li>
7
</ul>

That’s a Wrap!

Now you’re in the know: sorting values in JavaScript really isn’t as hard as you might have thought. You can imagine this being useful to sort a table, with something like this:

1

2
$('table#myTable thead th').toggle(
3
  function() {
4
    var $this = $(this);
5
    $('table#myTable tbody tr').datasort({
6
      datatype: $this.attr('rel'),
7
      sortElement: 'td.' + $this.attr('class')
8
    });
9
  }, 
10
  function() {
11
    var $this = $(this);
12
    $('table#myTable tbody tr').datasort({
13
      datatype: $this.attr('rel'), 
14
      sortElement: 'td.' + $this.attr('class'),
15
      reverse: true 
16
      });
17
  }
18
);

(Try replacing the jQuery code for the table in the first example with this!)

Of course, we could improve this plugin a lot; for example, we could have it check the rel atttribute for a datatype if one isn’t given as a parameter, and default to alpha if there is no rel. But that’s aside from the sorting.

In sum, to sort with JavaScipt, we follow these steps:

  1. Determine the different formats you want to sort.
  2. Decide what format you want to sort in.
  3. Sort the array of items with the sort() method, passing in a function that will convert the two items to your desired format before comparing them

Have a datatype to add to our plugin? Have a better way of sorting one of these? Let’s hear it in the comments!


Source link