Sorting Arrays and Collections in JavaScript

AID: 3586
  • Status: Published

8400 points

  • By
  • TypeTutorial
  • Posted on2010-08-18 at 22:42:46
Awards
  • Community Pick
  • Experts Exchange Approved
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.
fig2-6.jpg
  • 86 KB
  • Sorting on two fields of the record (Object)
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
                                    
1:
2:
3:
4:

Select allOpen 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 
                                    
1:
2:
3:
4:

Select allOpen 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
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

Select allOpen 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
    }
);   
                                    
1:
2:
3:
4:
5:

Select allOpen 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
}
                                    
1:
2:
3:
4:
5:
6:
7:
8:

Select allOpen 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
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:

Select allOpen 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
                                    
1:
2:
3:

Select allOpen 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;
}
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:

Select allOpen 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" )
];
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:

Select allOpen in new window


I wrote a little "display the data" function.  Here is the table before and after using the default sort:
fig2-1.jpg
  • 77 KB
  • The problem with "default" sorting of an object array
The problem with "default" sorting of an object array

Look 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 );
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

Select allOpen in new window


And the result is somewhat better:
fig2-2.jpg
  • 76 KB
  • The problem with string sorting -- unwanted case sensitivity
The problem with string sorting -- unwanted case sensitivity

However, 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 );
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

Select allOpen in new window


And the result is exactly what we want:
fig2-3.jpg
  • 78 KB
  • After making the sort case-insenstive
After making the sort case-insenstive

But 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 );
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:

Select allOpen in new window


This also works for sorting on nAwardLvl since it, too, has a numeric value.
fig2-4.jpg
  • 78 KB
  • Numeric sort, Ascending (perhaps not what's wanted)
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 );
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:

Select allOpen in new window


fig2-5.jpg
  • 78 KB
  • After coding to provide Descending sort
After coding to provide Descending sort

Note 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;
                                    
1:
2:
3:
4:
5:
6:

Select allOpen 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;
}
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:

Select allOpen in new window


So, here's our output when sorted by Author (one key) and then when sorted by Author and Title (two keys).
fig2-6.jpg
  • 86 KB
  • Sorting on two fields of the record (Object)
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;  
}
                                    
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:

Select allOpen in new window


And here is the complete code of an HTA (Hypertext Application) that uses these JavaScript techniques.
TestSort.HTA.txt
  • 4 KB
  • Rename to remove the .txt extension, then double-click to run the HTA
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!
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    Asked On
    2010-08-18 at 22:42:46ID3586
    Tags

    Array.sort

    ,

    JavaScript object

    ,

    lexical sort

    ,

    sorting numbers

    ,

    sorting dates

    ,

    Dan Rollins

    Topic

    JavaScript

    Views
    3007

    Comments

    Add your Comment

    Please Sign up or Log in to comment on this article.

    Join Experts Exchange Today

    Gain Access to all our Tech Resources

    Get personalized answers

    Ask unlimited questions

    Access Proven Solutions

    Search 3.2 million solutions

    Read In-Depth How-To Guides

    1000+ articles, demos, & tips

    Watch Step by Step Tutorials

    Learn direct from top tech pros

    And Much More!

    Your complete tech resource

    See Plans and Pricing

    30-day free trial. Register in 60 seconds.

    Loading Advertisement...

    Top JavaScript Experts

    1. leakim971

      511,289

      Sage

      2,168 points yesterday

      Profile
      Rank: Genius
    2. mplungjan

      291,279

      Guru

      2,800 points yesterday

      Profile
      Rank: Savant
    3. nap0leon

      195,491

      Guru

      0 points yesterday

      Profile
      Rank: Sage
    4. Proculopsis

      182,948

      Guru

      0 points yesterday

      Profile
      Rank: Sage
    5. COBOLdinosaur

      157,309

      Guru

      0 points yesterday

      Profile
      Rank: Genius
    6. chaituu

      130,684

      Master

      0 points yesterday

      Profile
      Rank: Sage
    7. Ray_Paseur

      130,217

      Master

      330 points yesterday

      Profile
      Rank: Savant
    8. tommyBoy

      125,345

      Master

      0 points yesterday

      Profile
      Rank: Genius
    9. StingRaY

      114,318

      Master

      0 points yesterday

      Profile
      Rank: Wizard
    10. DaveBaldwin

      80,081

      Master

      336 points yesterday

      Profile
      Rank: Genius
    11. ansudhindra

      79,054

      Master

      2,000 points yesterday

      Profile
      Rank: Wizard
    12. ChrisStanyon

      62,768

      Master

      800 points yesterday

      Profile
      Rank: Sage
    13. hielo

      61,266

      Master

      0 points yesterday

      Profile
      Rank: Savant
    14. HainKurt

      59,030

      Master

      0 points yesterday

      Profile
      Rank: Genius
    15. BuggyCoder

      54,739

      Master

      0 points yesterday

      Profile
      Rank: Sage
    16. mroonal

      54,339

      Master

      10 points yesterday

      Profile
      Rank: Sage
    17. tagit

      54,093

      Master

      1,600 points yesterday

      Profile
      Rank: Genius
    18. gurvinder372

      52,824

      Master

      10 points yesterday

      Profile
      Rank: Genius
    19. basicinstinct

      52,586

      Master

      0 points yesterday

      Profile
      Rank: Genius
    20. JonNorman

      45,158

      2,200 points yesterday

      Profile
      Rank: Master
    21. Lalit-Chandra

      44,420

      0 points yesterday

      Profile
      Rank: Master
    22. xmediaman

      36,450

      3,800 points yesterday

      Profile
      Rank: Guru
    23. kozaiwaniec

      33,100

      0 points yesterday

      Profile
      Rank: Guru
    24. Kravimir

      32,700

      0 points yesterday

      Profile
      Rank: Genius
    25. designatedinitializer

      32,300

      0 points yesterday

      Profile
      Rank: Master

    Hall Of Fame