Getting the Most Out of Array.sort()

ActionScript 2.0

Have you ever been surprised by the result of invoking Array.sort() on an array of numbers?  Curiously, numbers seem to be sorted in alphabetical order, which doesn’t seem intuitive.  A list of 4, 10, 2006, for example, sorts into 10, 2006, 4, rather than remaining unchanged, since the original is already sorted numerically.  From an alphabetical point of view, the “words” “4,” “10,” and “2006” would be arranged by their first “letters,” in which case it follows that “1” comes before “2,” and “2” comes before “4.”  But this is ridiculous, right?  What do you do if you want to sort numerically? 

Before we get to an answer, let’s take a look at how Array.sort() works.  What seems at first like alphabetical order is actually happenstance, a byproduct of a character’s position in the Unicode chart.  Since Flash MX (aka Flash 6), Array.sort() has compared characters according to their Unicode values.  Flash 5 used ISO-8859-1 (Latin 1) (p. 520, Moock’s ActionScript for Flash MX: The Definitive Guide, Second Edition).

So is there a way to override this behavior?  You bet.

Options

Flash MX 2004 (aka Flash 7) introduced sorting options that require Flash Player 7 or higher.  Since Flash 5, it has been possible to write one’s own sorting algorithms, passed in as a parameter to the method.  So it’s possible to emulate the new options in older players or create custom options as far back as Player 5.

Let’s start with one of the relatively new sorting options.  The Array class features a handful of property constants …

  • Array.NUMERIC
  • Array.CASEINSENSITIVE
  • Array.DESCENDING
  • Array.RETURNINDEXEDARRAY
  • Array.UNIQUESORT

… which may be passed as parameters to the Array.sort() method.

Numeric

A quick example:

var sample:Array = new Array(4, 10, 2006);
trace("Unsorted original:\\n" + sample + "\\n");
sample.sort();
trace("Array.sort():\\n" + sample + "\\n");
sample.sort(Array.NUMERIC);
trace("Array.sort(Array.NUMERIC):\\n" + sample);

Done.  Pretty easy.

To accomplish the same thing in Flash Player 5 and 6, we’ll have to use write a comparison function and pass that in, instead.  This function must return a number, which lets Array.sort() know which to put first when comparing element A to element B.  According to the docs, the number -1 puts A before B, 0 means they’re equal, and 1 puts A after B.

function custom(a:Object, b:Object):Number {
  return Number(a > b);
}

The expression a > b returns a Boolean (true or false).  Since true can be represented by 1 and false by 0, the above gives us the bare minimum required to tell Array.sort() to sort numerically.

var sample:Array = new Array(4, 10, 2006);
trace("Unsorted original:\\n" + sample + "\\n");
sample.sort();
trace("Array.sort():\\n" + sample + "\\n");
function custom(a:Object, b:Object):Number {
  return Number(a > b);
}
sample.sort(custom);
trace("Array.sort(custom):\\n" + sample);

Case Insensitive

All right, so about Array.CASEINSENSITIVE — what’s that for?  Again, characters are sorted by their Unicode values, so uppercase characters are positioned before lowercase characters.

var sample:Array = new Array("a", "MN", "z");
trace("Unsorted original:\\n" + sample + "\\n");
sample.sort();
trace("Array.sort():\\n" + sample);

If you don’t want capital letters to be considered that way, pass in the option.

var sample:Array = new Array("a", "MN", "z");
sample.sort(Array.CASEINSENSITIVE);
trace("Array.sort(Array.CASEINSENSITIVE):\\n" + sample);

To emulate this in older players, construct a new custom function.  One way to accomplish a case insensitive sort is to level the playing field by converting elements A and B to lowercase (or uppercase) before comparing.

var sample:Array = new Array("a", "MN", "z");
function custom(a:Object, b:Object):Number {
  return Number(a.toLowerCase() > b.toLowerCase());
}
sample.sort(custom);
trace("Array.sort(custom):\\n" + sample);

Note:  Don’t worry if your array includes numbers.  The String.toLowerCase() method won’t do anything to a number.  Numbers will, however, sort before characters.

Descending

The Array.DESCENDING option sorts array elements in reverse order.  Calling it “Z-to-A” is a bit misleading, because this would actually be the reverse order of the Unicode chart, but you get the idea.  This should start to look familiar, by now.

var sample:Array = new Array("a", "b", "c");
sample.sort(Array.DESCENDING);
trace("Array.sort(Array.DESCENDING):\\n" + sample);

In this case, emulation is pretty simple.  Just sort the array normally, then use Array.reverse() to reverse it.

var sample:Array = new Array("a", "b", "c");
sample.sort();
sample.reverse();
trace("Array.sort() and Array.reverse():\\n" + sample);

Return Indexed Array

Invoking Array.sort() on an Array instance actually changes that array.  If you want to leave the original array untouched (unsorted), and basically sort a clone, use the Array.RETURNEINDEXEDARRAY option.

var sample:Array = new Array("c", "b", "a");
sample.sort(Array.RETURNEINDEXEDARRAY);
trace("Array.sort(Array.RETURNINDEXEDARRAY):");
trace(sample.sort(Array.RETURNINDEXEDARRAY));
trace("\\nOriginal remains unsorted:\\n" + sample);

Now, wait a minute!  The first trace puts 2,1,0 to the Output panel.  What’s that?  It sure isn’t a,b,c, which is what you might expect.  Well, this output is effectively a “preview,” some assembly required, of how the original array would look if sorted.  In other words, it tells you, “Hey, programmer, if you had sorted this array proper, element 2 (the original third) would have appeared first, element 1 would have appeared second, and element 0 (the original first) would have appeared third.”

To be honest, I’m not sure how useful this sort of sort is (that was a pun!).  Emulating this seems like more effort than it’s worth, to me, but here’s a go at it.  You could write a function to clone your array in reverse order.

var sample:Array = new Array("c", "b", "a");
function cloneArray(a:Array):Array {
  var clone:Array = new Array();
  var count:Number = a.length;
  while (count) {
    clone.push(a[--count]);
  }
  return clone;
}
trace("cloneArray()\\n" + cloneArray(sample));
trace("\\nOriginal remains unsorted:\\n" + sample);

That gives you gives you what I wish Array.RETURNINDEXEDARRAY did; namely, the sorted array itself, no assembly required, without touching the original.  To match the option feature exactly:

var sample:Array = new Array("c", "b", "a");
function cloneArray(a:Array):Array {
  var clone:Array = new Array();
  var count:Number = a.length;
  while (count) {
    clone.push(--count);
  }
  return clone;
}
trace("cloneArray()\\n" + cloneArray(sample));
trace("\\nOriginal remains unsorted:\\n" + sample);

Unique Sort

The Array.UNIQUESORT option returns 0 if any duplicates exist in the array; otherwise, it sorts as usual.  If duplicates exist, the original array is untouched (only 0 is returned).

var sample:Array = new Array("c", "b", "a", "a");
sample.sort(Array.UNIQUESORT);
trace("Array.sort(Array.UNIQUESORT):");
trace(sample.sort(Array.UNIQUESORT));
trace("\\nOriginal remains unsorted:\\n" + sample);
var sample:Array = new Array("c", "b", "a");
// no duplicates
sample.sort(Array.UNIQUESORT);
trace("Array.sort(Array.UNIQUESORT):");
trace(sample.sort(Array.UNIQUESORT));
trace("\\nOriginal is also sorted:\\n" + sample);

To emulate this, compare each element, in turn, with other elements to look for a duplicate.  As soon as one is found, return 0; otherwise, return the sorted array.

var sample:Array = new Array("c", "b", "a", "a");
function uniqueSort(a:Array):Object {
  var count:Number = a.length;
  var i:Number;
  var j:Number = count - 1;
  for (i = 0; i < count; i++) {
    for (j = i + 1; j < count; j++) {
      if (a[i] === a[j]) return 0;
    }
  }
  return a.sort();
}
trace("uniqueSort()\\n" + uniqueSort(sample));
trace("\\nIf duplicates, original remains unsorted:\\n" + sample);

Note the strict equality operator, ===, which ensures that array elements are only considered duplicates if their datatypes match in addition to their values.  Test this same function with the following arrays to see what this means.

var sample:Array = new Array("c", "b", "a", "1", 1);
var sample:Array = new Array("c", "b", "a", 1, 1);

Combinations

Array options may be combined with the pipe character,|.

var sample:Array = new Array("a", "MN", "z", 3, 2, 1);
sample.sort(Array.NUMERIC | Array.CASEINSENSITIVE);
trace(sample);
// output: 1,2,3,a,MN,z

If you want to emulate something like this — that is, something of complexity — cut loose with your custom function  Here is an example that sorts numeric and case insensitive and puts the letters first.

var sample:Array = new Array(1, 100, 11, 2000, 22, "MN", "a", "z");
trace("Unsorted original:\\n" + sample + "\\n");
function custom(a:Object, b:Object):Number {
  var output:Number;
  if (typeof a != typeof b) {
    if (typeof a == "string") output = -1;
    if (typeof a == "number") output = 1;
  } else {
    output = Number(a.toLowerCase() > b.toLowerCase());
  }
  return output;
}
sample.sort(custom);
trace("Array.sort(custom):\\n" + sample);

Leave a Reply