<

Still celebrating National IT Professionals Day with 3 months of free Premium Membership. Use Code ITDAY17

x

Sorting Arrays and Collections in JavaScript

Published on
24,632 Points
14,532 Views
6 Endorsements
Last Modified:
Awarded
In this article, we'll look how to sort an Array in JavaScript, including the more advanced techniques of sorting a collection of records either ascending or descending on two or more fields.
Sorting on two fields of the record (Object)

Basic Sorting of Arrays

First, let's look at the basics.  It's all about the sort() method of the Array object.  The following example defines an array of person names, sorts them using the default handling, and displays the result:
var aNames= [ "Bob", "Jim", "Sam", "Al" ];
alert( aNames.toString() ); // (unsorted) shows: Bob,Jim,Sam,Al
aNames.sort();              // sort the array
alert( aNames.toString() ); // (sorted) shows: Al,Bob,Jim,Sam

Open in new window

As shown in that example, the sort is a "lexical" sort -- it sorts alphabetically in ascending (A-Z) order.  That's great for sorting names, but not so wonderful when sorting numbers or other data types.  For instance:
var aNumbers= [ 900, 17, 3, 1002 ];
alert( aNumbers.toString() );  // (unsorted) shows: 900,17,3,1002
aNumbers.sort();               // sort the array
alert( aNumbers.toString() );  // (sorted???) shows: 1002,17,3,900 

Open in new window

About the only thing that's good about that result is that it makes it easy for your JavaScript professor to give you a failing grade!  It's even worse if you are sorting date fields.  The default sort turns a Date value into words, like:

    "Sun Nov 1..." which will be sorted after
    "Mon Nov 2..."

But that's just the default behavior.  Array.sort() lets you customize the action by specifying a function that it will use as it sorts.  In the next example, we provide a comparison function so that the sort will work for numeric values:
function CompareNumbers( v1, v2 ) {
    if ( v1 < v2 ) return -1;  // v1 goes nearer to the top
    if ( v1 > v2 ) return 1;   // v1 goes nearer to the bottom
    return 0;                  // both are the same
}
...
var aNumbers= [ 900, 17, 3, 1002 ];
alert( aNumbers.toString() );      // (unsorted) shows: 900,17,3,1002
aNumbers.sort( CompareNumbers );   // sort the array
alert( aNumbers.toString() );      // (sorted!!!) shows: 3,17,900,1002

Open in new window

The comparison function gets called many times during the sorting operation.  Each time, it is passed two values (v1 and v2).  All you need to do is decide which value is higher and return an indicator:
Return a negative number (say, -1) if v1 is less than v2
Return a positive number (say, 1)  if v1 is greater than v2
Return 0 if v1 and v2 are equal for sorting purposes

(Mnemonic:  "I'm positive it's greater!")
You will often see a shortcut for numeric values -- if you subtract v2 from v1, the result is positive or negative or zero, exactly as desired for an ascending numeric sort.  It's also possible to create an anonymous function right in the sort call itself.  Here is an example of those two concepts combined:
aNumbers.sort( 
    function(v1,v2){
        return v1-v2; // sort numbers ascending
    }
);   

Open in new window


Sorting Descending vs. Ascending

Sorting in reverse order (Z..A or 9...0) is (or at least, should be) easy.  In your comparison function, just reverse the logic used in normal (A..Z) sorting.  But I have to admit that I get confused... was I supposed to subtract v1 from v2, or am I supposed to pretend that v1 is lower, to make it go higher in the list, or... all of these musings just make it hard to remember what to do.  However... There is a very easy trick:

Use the normal (ascending) comparison logic, but swap the parameters!
function MyCompare( v1, v2 ) {
    if ( gfSortDescending ) {
        var t=v1; v1=v2; v2=t; // swap the parms
    }
    if ( v1 < v2 ) return -1;  // v1 goes nearer to the top
    if ( v1 > v2 ) return 1;   // v1 goes nearer to the bottom
    return 0;                  // both are the same
}

Open in new window


Sorting Dates

As mentioned earlier, if your array data consists of date values, the normal (default) handling of Array.sort() will fail disastrously.  So, if you sort on a date field, you must supply a comparison function.  Assuming that the array items are actual Date datatype values, you can use normal comparison logic -- JavaScript's < and > operators work perfectly for Date object values:
function CompareDates( d1, d2 ) {
    if ( d1 < d2 ) return -1; // d1 is in the past of d2
    if ( d1 > d2 ) return 1;  // d1 is in the future of d2
    return 0;
}
...

var dt1= new Date('10/31/2010');
var dt2= new Date('11/01/2010');
var dt3= new Date('1/1/2011');

var aDates= [dt1, dt2, dt3 ]; 
aDates.sort( CompareDates ); 
alert( aDates.toString() );  // sorted correctly in ascending order

Open in new window

Another commonly-used option:  Avoid storing Date objects in the array, and instead, store text strings that are in an implicitly sortable format: YYYY-MM-DD
var aDateStrings= ["2010-10-31", "2010-11-01", "2011-01-01" ]; 
aDateStrings.sort();     // OK to use the default (string) sort
alert( aDateStrings.toString() );  // sorted in ascending order

Open in new window

The trick with this technique is to be sure that all of the dates are formatted correctly with four-digit years, and two digits (including leading zero if needed) for the month and the day.


Sorting Collections (Records)

As we discussed in the last installment, it is a common programming task to work with an array of objects, in which each object is a record, containing several fields.  The usual scenario is to display such data in a table, and allow the user to be able to sort it various ways.  We'll look at how to sort by any single field, then reverse the order (sort descending) on a single field, and finally, we'll see how to sort on two fields -- a primary and a secondary.

Our data record will be a collection of information about articles at Experts Exchange.  Here is the "defining" structure (the constructor function that we'll use as a structure definition):
function ArticleRec(i,a,t,aw)
{
    this.nID=       i;
    this.sAuthor=   a;
    this.sTitle=    t;
    this.aAwards=   aw;    // EC, EEA, CP, or (empty)
    this.nAwardLvl= 0;     // (none)
    if (this.aAwards.indexOf("CP")  > -1) this.nAwardLvl = 1;
    if (this.aAwards.indexOf("EEA") > -1) this.nAwardLvl = 2;
    if (this.aAwards.indexOf("EC")  > -1) this.nAwardLvl = 3;
}

Open in new window

Note the addition of some program logic (lines 8-10) that generates an integer value based on the award level.  This constructor logic saves some steps later when we want to sort the record array.

Here is our sample dataset, defined in source code (it would typically be read from a web page or a database):
var arArticles= [  
  new ArticleRec(1023,"matthews...","Switch in Access", "EEA,CP" ),
  new ArticleRec(501, "DanRollins", "ABCs of JavaScript","" ),
  new ArticleRec(2001,"matthews...","Pivot Tables", "EEA,CP" ),
  new ArticleRec(1700,"DanRollins", "Writing Limericks", "CP" ),
  new ArticleRec(2590,"DanRollins", "C++ Multithreading","EEA" ),
  new ArticleRec(3111,"Harfang", "Printing Labels", "EC,EEA,CP" ),
  new ArticleRec(2901,"demazter", "Offline Defrag", "EEA,CP" ),
  new ArticleRec(17, "matthews...","PMT, FV, and PPMT", "EC,EEA,CP" ),
  new ArticleRec(1517,"angelIII", "UPDATES with JOIN", "EC,EEA" )
];

Open in new window

I wrote a little "display the data" function.  Here is the table before and after using the default sort:
The problem with "default" sorting of an object arrayLook closely --  the default sort seems to randomize the list!  That's because without a comparison function, each array item is converted to the text "[Object object]" for comparison purposes, and that's not useful at all.

Here's a simple version of a comparison function for our array of records:
var gsSortBy="";  // a field name -- global var for now

MyCompare= function(v1,v2) {
    if ( v1[gsSortBy] < v2[gsSortBy] ) return -1; 
    if ( v1[gsSortBy] > v2[gsSortBy] ) return 1; 
    return 0;  
}
...
gsSortBy="sAuthor";  // set the sort key
arArticles.sort( MyCompare );

Open in new window

And the result is somewhat better:
The problem with string sorting -- unwanted case sensitivityHowever, we'd probably prefer to see the sort that ignored character case so that, for instance, "angleIII" is at the top of the list.   Here's a variation that does that:
var gsSortBy="";  // a field name -- global var for now

MyCompareNoCase= function(v1,v2) {
    if ( v1[gsSortBy].toLowerCase() < v2[gsSortBy].toLowerCase() ) return -1; 
    if ( v1[gsSortBy].toLowerCase() > v2[gsSortBy].toLowerCase() ) return 1; 
    return 0;  
}
...
gsSortBy= "sAuthor";  
arArticles.sort( MyCompareNoCase );

Open in new window

And the result is exactly what we want:
After making the sort case-insenstiveBut that causes a problem if we want to sort of a numeric field -- the toLowerCase() function expects a string, not a number and using it on a numeric value will cause an error.  Here's how to handle that:  Add logic in the comparison function that checks the data type being compared...
var gsSortBy="";  // a field name -- global var for now

MyCompareNoCaseAndNumbers= function( v1,v2 ) {      // two objects
    if ( typeof v1[gsSortBy]=="number" ) { // when sorting by a numeric field
       if ( v1[gsSortBy] < v2[gsSortBy] ) return -1; 
       if ( v1[gsSortBy] > v2[gsSortBy] ) return 1; 
       return 0;  
    } 
    else {  // assume sort field is a String
        if ( v1[gsSortBy].toLowerCase() < v2[gsSortBy].toLowerCase() ) return -1; 
        if ( v1[gsSortBy].toLowerCase() > v2[gsSortBy].toLowerCase() ) return 1; 
        return 0;  
    }
}
...
gsSortBy= "nID";  // set the sort key
arArticles.sort( MyCompareNoCaseAndNumbers );

Open in new window

This also works for sorting on nAwardLvl since it, too, has a numeric value.
Numeric sort, Ascending (perhaps not what's wanted)It's more likely that a person would want to see the articles sorted with the "best" at the top.  The nAwardLvl field has been set up so that higher values mean higher accolades.  So if we were to sort by descending values, then we'd get what we want.  That means just inserting the swapping logic we covered earlier, like so:
MyCompareNoCaseAndNumbers= function(v1,v2) {
    if (gfSortDescending) {
        var t=v1; v1=v2; v2=t; // swap the parms
    }
    // do the comparison here
    ...
}
...
gsSortBy= "nAwardLvl";       // global var for now
gfSortDescending= true;        // global var for now
arArticles.sort( MyCompareNoCaseAndNumbers );

Open in new window

After coding to provide Descending sortNote that "EEA,CP" is considered equal to "EEA-only" by our simplistic evaluator in the constructor (both cause nAwardLvl to be set to 2).   I'll leave it as an exercise to the student to rewrite the constructor so that when sorted by nAwardLvl, "EEA,CP" will always be "higher" than "EEA-only."


Sorting on Two Fields

When there are many records and when multiple records might have the same value, you'll want to provide a means for the user to do a secondary-field sort.  For instance, the telephone book has lots of entries for people with the surname "Jones" in displaying that list, you most certainly want to do a secondary sort on the first name.

One way to handle that is to have your comparison function synthesize a combined value; for instance:
var sKey1= v1.sNameLast + v1.sNameFirst;
var sKey2= v2.sNameLast + v2.sNameFirst;

if (sKey1 > sKey2 ) return 1;
if (sKey1 < sKey2 ) return -1;
return 0;

Open in new window

Though simple, that's not very flexible; for one thing, it fails for numeric sorts.  To create a generalized solution, just think about this:  The secondary sort comes into play only when the two primary sort keys are equal.  So all you need to do is add some logic to the comparison function where you would normally return 0.  Here's a simplified example of what I mean:
// uses global variables gsSortyBy and gsSortBy2

MyCompareTwoKeys= function(v1,v2) {
    if ( v1[gsSortBy] < v2[gsSortBy] ) return -1; 
    if ( v1[gsSortBy] > v2[gsSortBy] ) return 1; 
    // else, they are equal
    if ( gsSortBy2 > "" ) {  // a secondary sort field has been set
       if ( v1[gsSortBy2] < v2[gsSortBy2] ) return -1; 
       if ( v1[gsSortBy2] > v2[gsSortBy2] ) return 1; 
   }
   return 0;
}

Open in new window

So, here's our output when sorted by Author (one key) and then when sorted by Author and Title (two keys).
Sorting on two fields of the record (Object)
Note:
To sort by three keys, just add another comparison sequence when the two secondary keys are equal.
At long last, here's the "final" code that includes:

String sort that ignores character case
Numeric sort that works as expected (e.g., 99 < 100)
Sort Ascending (AtoZ) or descending (ZtoA)
Ability to specify which field to use as the sort key
Ability to specify a secondary sort
Uses "local" object attributes of the Array object to avoid global variables
// Assign some default attributes to the array object
// This lets us keep the data together rather than use global variables
//
arArticles.sortBy=  "nAwardLvl";  // a field name in an ArticleRec object
arArticles.sortDir= "AtoZ";       // "AtoZ" or "ZtoA" 
arArticles.sortBy2= "sAuthor";
arArticles.sortDir2="AtoZ";
//-------------------------------------- the sort function for this array
arArticles.Compare= function(v1,v2) {
    if ( arArticles.sortDir == "ZtoA" ) {
        var t=v1; v1=v2; v2=t; // swap the parms
    }
    var sFld= arArticles.sortBy;
    if ( typeof v1[sFld]=="number" ) {      
       if ( v1[sFld] < v2[sFld] ) return -1; 
       if ( v1[sFld] > v2[sFld] ) return 1;
    } 
    else { // string
       if ( v1[sFld].toLowerCase() < v2[sFld].toLowerCase() ) return -1; 
       if ( v1[sFld].toLowerCase() > v2[sFld].toLowerCase() ) return 1; 
    }
    //------------------------------- check for and handle secondary sort key
    if ( arArticles.sortBy2 > "" ) {
        if ( arArticles.sortDir=="AtoZ" && arArticles.sortDir2=="ZtoA" ) {
            var t=v1; v1=v2; v2=t; // swap the parms
        }
        sFld= arArticles.sortBy2;
        if ( typeof v1[sFld]=="number" ) { 
           if ( v1[sFld] < v2[sFld] ) return -1; 
           if ( v1[sFld] > v2[sFld] ) return 1;
        }
        else { // string
            if ( v1[sFld].toLowerCase() < v2[sFld].toLowerCase() ) return -1; 
            if ( v1[sFld].toLowerCase() > v2[sFld].toLowerCase() ) return 1; 
        }
    }
    return 0;  
}

Open in new window

And here is the complete code of an HTA (Hypertext Application) that uses these JavaScript techniques.
TestSort.HTA.txt
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
If you liked this article and want to see more from this author, please click the Yes button near the:
      Was this article helpful?
label that is just below and to the right of this text.   Thanks!
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
6
Comment
Author:DanRollins
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
0 Comments

Featured Post

Free Tool: Site Down Detector

Helpful to verify reports of your own downtime, or to double check a downed website you are trying to access.

One of a set of tools we are providing to everyone as a way of saying thank you for being a part of the community.

Join & Write a Comment

The viewer will learn the basics of jQuery, including how to invoke it on a web page. Reference your jQuery libraries: (CODE) Include your new external js/jQuery file: (CODE) Write your first lines of code to setup your site for jQuery.: (CODE)
The viewer will learn the basics of jQuery including how to code hide show and toggles. Reference your jQuery libraries: (CODE) Include your new external js/jQuery file: (CODE) Write your first lines of code to setup your site for jQuery…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month