Getting the Most Out of Array.sort()
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.NUMERICArray.CASEINSENSITIVEArray.DESCENDINGArray.RETURNINDEXEDARRAYArray.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);
April 12th, 2006 at 5:35 am
var _array:Array = new Array(1,2,3,4,5)
_array.sort( function() { return random(2)>0? 1:-1; } );
trace(_array)
Can you tell me how does it work?
April 12th, 2006 at 8:47 am
木頭,
Your example makes use of the custom sorting algorithm described in the article. In your case, you’re supplying the parameter as a function literal, which is perfectly fine. The function itself relies on two factors: the global
random()function, which returns a pseudo-random integer between zero and one less than the number given, and the conditional operator.The ActionScript Language Reference recommends the
Math.random()method over therandom()function, which was deprecated in Flash 5.Math.random()is a bit different in that it accepts no parameters and returns a pseudo-random float greater than or equal to zero and less than one. You would need to useMath.round()in addition toMath.random()to achieve the same effect asrandom()alone.The conditional operator works like this …
(expression) ? result1 : result2
… in which an expression is evaluated as true or false. If
true, the first result is returned; iffalse, the second is returned.In your case, the expression is
random(2), which I’ll update to the newer version …Remember, the number 1 represents
trueand 0 representsfalse, so that provides what the conditional operator needs. If the expression evaluates totrue, the number 1 will be returned, which means element A will be positioned ahead of element B. If the expression evaluates tofalse, the number -1 will be returned, which means the opposite.So rather than an expression like
a > b, which is entirely predictable (element A is either greather than element B or it isn’t), you’re providing a random algorithm, which effectively “shuffles” your array.April 17th, 2006 at 11:01 am
It’s pretty inefficient though.
Here’s a nice little discussion on different sort methods people came up with, and the best of one of all as well:
http://groups.google.com/group/macromedia.flash.actionscript/
browse_frm/thread/831835576870f132?tvc=1&hl=en
April 17th, 2006 at 11:56 am
NSurveyor,
Thanks for turning that one up again! I knew that was out there, but hadn’t been able to find it in a while.
April 30th, 2006 at 9:13 am
Thanks for the example of how to define the list as numeric when you sort it - this has solved a problem for me where I had 10
April 30th, 2006 at 2:11 pm
Glad to hear it, Tom!
May 16th, 2006 at 6:20 pm
Before I knew how it worked, I was amazed how the sort allowed you to give the options seperated by |. It’s rather simple. All the options are actually numbers - specifically, powers of 2. Array.CASEINSENSITIVE is 1. Array.DESCENDING is 2. Array.UNIQUESORT is 4. Array.RETURNINDEXEDARRAY is actually 8. Array.NUMERIC is 16. In binary, they would be: 00001, 00010, 00100, 01000, 10000. expression1 | expression2 will convert “expression1 and expression2 to 32-bit unsigned integers, and returns a 1 in each bit position where the corresponding bits of either expression1 or expression2 are 1.” So…. 00001 | 00100 would give 00101 (or 5). Given this number, Flash can determine the options to use.
April 19th, 2007 at 4:59 pm
David,
I’ve a problem with the randomize move from label to label . My need is the same that the Brian (february 10th) but with 49 elements in the Array. “My” function is equal to that your reply to Brian’s question, except by the number of elements (frame labels).
Everything in function works perfectly, until in 25th execution of getRandomLabel. In that moment the functions trace show in the list a “undefined” result. The trace of labels.length correctly shows the amount to it of elements of the Array.
Can you help me with some possible solution for my problem?
Your blog has been helpful in my learning.
Thank you very much.
Marcos Paim
May 8th, 2007 at 9:01 pm
Marcos,
That sounds like you’re looping through the array until equal to its length rather than less than its length.
March 11th, 2008 at 3:58 am
Hmm - I’m using a randomizer as the sort function - ie an easy array scrambler function. My confusion is when using array sort in a class: ie
in the class constructor method :
this_array.sort(randomsort);
and later in the class:
function randomrange(minval, maxval) {
return minval + Math.floor(Math.random() * (maxval + 1 - minval)); }
function randomsort() {
//pass a function to sort makes it the sort criteria.. in this case just shuffle..
var t = randomrange(-1, 1);
return t
}
March 12th, 2008 at 8:54 pm
julz_hk,
I’m afraid I may not have fully understood your question. I think you’re asking how to use your
randomsort()function when it’s declared as the method of a custom class. If so, you need to create an instance of this class, then reference the class’s method. If this class is namedCustomClass, for example, you would do this: