Sorting ColdFusion Arrays With Sortable Interfaces

Posted November 7, 2006 at 8:01 AM

Tags: ColdFusion

Richard over on CF-Talk asked about sorting arrays using a deep attribute. As far as I know, there is nothing built into ColdFusion that does this kind of thing automatically. I know that ColdFusion's StructSort() method allows the user to provide a path to the sub element on which the sorting is based, but there doesn't seem to be anything like this for Arrays.

This got me thinking; I am pretty sure I remember something about a "Sortable" interface that exists in other languages. This sounded like something that would be helpful for this situation. After a bit of experimentation, here is what I came up with. You need to have an abstract class "AbstractSortable". This defines the following comparative methods:

AbstractSortable::LT()
AbstractSortable::LTE()
AbstractSortable::EQ()
AbstractSortable::GTE()
AbstractSortable::GT()

It also has some other methods:

AbstractSortable::Init()
AbstractSortable::SetTarget()
AbstractSortable::SortArray()

The comparative methods are used during the sorting algorithm defined in the SortArray() method (which in our case is going to be a bubble sort).

One you have all that set up, you need a concrete class the overrides at least one of the comparative methods. Each comparative method that is overridden must take an object reference to which we will be comparing the concrete class's target object. Each method must return a boolean. For every "True" returned, the two items in comparison will be swapped.

Now that I have given a brief explanation, let's take a look at some code. Here is the abstract sortable class:

AbstractSortable.cfc ColdFusion Component

 Launch code in new window » Download code as text file »

  • <cfcomponent
  • displayname="AbstractSortable"
  • output="false"
  • hint="Defines the interface and base methods for a sortable object.">
  •  
  • <!--- Run the pseudo constructor to set up default data structures. --->
  • <cfscript>
  •  
  • // Set up an instance structure to hold instance data.
  • VARIABLES.Instance = StructNew();
  •  
  • // Set the target object to which other objects will be compared.
  • VARIABLES.Instance.Target = "";
  •  
  • </cfscript>
  •  
  •  
  • <cffunction name="Init" access="public" returntype="any" output="false"
  • hint="Returns an initialized AbstractSortable instance.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Target" type="any" required="false" default="" />
  •  
  • <!--- Store the arguments. --->
  • <cfset VARIABLES.Instance.Target = ARGUMENTS.Target />
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction name="SetTarget" access="public" returntype="void" output="false"
  • hint="Sets the new target object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Target" type="any" required="true" />
  •  
  • <!--- Store the arguments. --->
  • <cfset VARIABLES.Instance.Target = ARGUMENTS.Target />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction name="SortArray" access="public" returntype="array" output="false"
  • hint="Sorts an array using this sortable instance.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Data" type="array" required="true" />
  • <cfargument name="Method" type="string" required="true" />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  •  
  • <!--- Perform a bubble sort. --->
  • <cfloop index="LOCAL.OuterIndex" from="1" to="#(ArrayLen( ARGUMENTS.Data ) - 1)#" step="1">
  •  
  • <cfloop index="LOCAL.InnerIndex" from="1" to="#(ArrayLen( ARGUMENTS.Data ) - LOCAL.OuterIndex)#" step="1">
  •  
  • <!--- Set the target to which we will comapre objects. --->
  • <cfset THIS.SetTarget( ARGUMENTS.Data[ LOCAL.InnerIndex ] ) />
  •  
  • <!--- Get the method that we are going to call for comparison. --->
  • <cfset LOCAL.Method = THIS[ ARGUMENTS.Method ] />
  •  
  • <!--- Compare to next object using the requested method. --->
  • <cfif LOCAL.Method( ARGUMENTS.Data[ LOCAL.InnerIndex + 1 ] )>
  •  
  • <!--- Swap the two indexed objects. --->
  • <cfset ArraySwap(
  • ARGUMENTS.Data,
  • LOCAL.InnerIndex,
  • (LOCAL.InnerIndex + 1)
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfloop>
  •  
  •  
  • <!--- Return the updated array. --->
  • <cfreturn ARGUMENTS.Data />
  • </cffunction>
  •  
  •  
  • <cffunction name="LT" access="public" returntype="boolean" output="false"
  • hint="Determins if this object is less than the passed in object.">
  • </cffunction>
  •  
  •  
  • <cffunction name="LTE" access="public" returntype="boolean" output="false"
  • hint="Determins if this object is less than or equal to the passed in object.">
  • </cffunction>
  •  
  •  
  • <cffunction name="EQ" access="public" returntype="boolean" output="false"
  • hint="Determins if this object is equal to the passed in object.">
  • </cffunction>
  •  
  •  
  • <cffunction name="GTE" access="public" returntype="boolean" output="false"
  • hint="Determins if this object is greater than or equal to the passed in object.">
  • </cffunction>
  •  
  •  
  • <cffunction name="GT" access="public" returntype="boolean" output="false"
  • hint="Determins if this object is greater than the passed in object.">
  • </cffunction>
  •  
  • </cfcomponent>

And, here is the sortable girl class which is a concrete instantiation of the AbstractSortable.cfc:

SortableGirl.cfc ColdFusion Component

 Launch code in new window » Download code as text file »

  • <cfcomponent
  • displayname="SortableGirl"
  • extends="AbstractSortable"
  • output="false"
  • hint="Defines the concrete sortable class for a girl.">
  •  
  •  
  • <cffunction
  • name="GT"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="Determins if this object is greater than the passed in object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Comparable"
  • type="any"
  • required="true"
  • />
  •  
  • <!--- Check by birthday only. --->
  • <cfreturn (
  • VARIABLES.Instance.Target.Birthday GT
  • ARGUMENTS.Comparable.Birthday
  • ) />
  • </cffunction>
  •  
  • </cfcomponent>

Notice that the SortableGirl.cfc component extends the AbstractSortable.cfc component. This means that the SortableGirl.cfc has all the methods of the AbstractSortable.cfc but overrides only the ones it defines. In our case, we are overriding the GT() method. This means that we intend to sort something in an ASCending manner. But, look at how we compare the two items; we are comparing the "Birthday" properties of the given objects. This is where you do the custom sorting. I use Birthday, but this could just have easily been something crazy like:

 Launch code in new window » Download code as text file »

  • ((ARGUMENTS.Comparable.Foo.Bar.Blam() + 3) MOD 7)

The point is, you can do any sort of comparison on the two objects you want so long as you return TRUE or FALSE.

Ok, now let's take a look at the sorting in action. I have set up an array of girls and given them random Birthdays:

 Launch code in new window » Download code as text file »

  • <!---- Create an array to hold the girls. --->
  • <cfset arrGirls = ArrayNew( 1 ) />
  •  
  • <!--- Create a list of names. --->
  • <cfset lstGirls = "Molly,Libby,Julie,Sarah,Cindy,Donna" />
  •  
  • <!---
  • Loop over the list of names and add a girl
  • with a random birthday. We are going to get the random
  • birthday by adding a random number of days to the
  • beginnig of this year.
  • --->
  • <cfloop
  • index="strName"
  • list="#lstGirls#"
  • delimiters=",">
  •  
  • <!--- Create a new girl struct. --->
  • <cfset objGirl = StructNew() />
  •  
  • <!--- Set name. --->
  • <cfset objGirl.Name = strName />
  •  
  • <!--- Set a random birthday this year. --->
  • <cfset objGirl.Birthday = DateFormat(
  • ("2006-01-01" + RandRange( 0, 364 )),
  • "mmm d, yyyy"
  • ) />
  •  
  • <!--- Add girl to array. --->
  • <cfset ArrayAppend( arrGirls, objGirl ) />
  •  
  • </cfloop>

Dumping out the arrGirls array we get:


 
 
 

 
 
 
 
 

Notice that the girls are arranged in their original list sort, having nothing to do with their birthdays. Now, let's create an instance of the SortableGirl.cfc ColdFusion Component and use it to sort the array:

 Launch code in new window » Download code as text file »

  • <!--- Create a sort object. --->
  • <cfset objGirlSorter = CreateObject(
  • "component",
  • "SortableGirl"
  • ).Init() />
  •  
  •  
  • <!--- Sort the array. --->
  • <cfset arrGirls = objGirlSorter.SortArray(
  • arrGirls,
  • "GT"
  • ) />

Once we instantiation the SortableGirl.cfc (which extends AbstractSortable.cfc) we call the method SortArray(). We pass in the array that we want to sort and the method we want to use for sorting (GT()). This will perform the Bubble Sort defined in the AbstractSortable.cfc and will give us the following:


 
 
 

 
 
 
 
 

Now, all the girls are sorted in an ASCending order based on their birthdays. To sort in a DESCending order, all you would have to do is override the LT() method and run the sort again. The only down side to this is that you have to create a class that defines the way you want to sort. But, if your sort is going to be highly customized, this minor overhead is going to be worth the usability of it.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Other Searches  |  Print Page



Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

Dec 7, 2007 at 5:17 PM // reply »
4 Comments

Ben, just wanted to let you know, a year later, I found this very useful. Thanks!


Dec 10, 2007 at 8:01 AM // reply »
7,572 Comments

@Rob,

As always, I'm glad that I could help. Feel free to drop a line anytime you get stuck on something.


Aug 4, 2009 at 11:35 AM // reply »
3 Comments

I like what you did but I think there is a bit less code version:

<cfscript>
//before
myStruct = structNew();

myStruct.Contractor = ArrayNew(1);
myStruct.City = ArrayNew(1);
myStruct.miles = ArrayNew(1);

contractorList = 'a,b,c,d,e,f,g,h,i';
cityList = 'Maui,Oahu,Sydney,Los Angeles,Istanbul,Bodrum,Paris,London,Budapest';
milesList = '213,12,1500,701,2300,2100,1800,1900,2150';
for (i=1; i<10; i++) {
myStruct.Contractor[i] = ListGetAt(contractorList,i);
myStruct.City[i] = ListGetAt(cityList,i);
myStruct.miles[i] = ListGetAt(milesList,i);
}
</cfscript>
<cfdump var="#myStruct#">

<cfscript>
//after
session.myStruct = structNew();

for (j=1; j<= #arrayLen(myStruct.miles)#; j++) {
for (k=1; k<= #arrayLen(myStruct.miles)#; k++) {
if(myStruct.miles[j] lt myStruct.miles[k]) {
ArraySwap(myStruct.miles,k,j);
ArraySwap(myStruct.Contractor,k,j);
ArraySwap(myStruct.City,k,j);
}
}
}
session.myStruct = myStruct;
</cfscript>

<cfdump var="#session.myStruct#">


Aug 5, 2009 at 8:41 AM // reply »
7,572 Comments

@Falconseye,

I like what you're doing, but I am not sure that it is all that much less code. Once you get the base sorter component out of the way, the CFC that I define for the actual Girl array is rather short - practically just one argument and a return statement of a boolean condition. Plus, it's reusable. If you're only going to use your solution once, that's cool, but wrapping it up in a CFC allows the solution to be used in multiple places without any duplication.


Feb 26, 2010 at 11:03 AM // reply »
2 Comments

Thanks Ben, This solution is exactly what I've been looking for. The approach is perfect.


Feb 27, 2010 at 11:43 AM // reply »
7,572 Comments

@Patrick,

Awesome! What kind of stuff were you sorting (just out of curiosity)?


Mar 1, 2010 at 9:32 AM // reply »
2 Comments

@Ben

I'm sorting air traffic data in fairly large quantities. I'm currently receiving the data as JSON and displaying it in an ExtJS Grid Panel. The only problem is allowing the ExtJS Grid Panel to do the sorting on the client side is naturally very slow when its a couple thousand records. I'm looking to make sorting capability appen on the server-side. I'm also looking to implement paging as well to limit the number of records returned to the client at a time. I haven't implemented it yet, but I believe your solution will do the trick.

Thanks again for posting this.


Mar 2, 2010 at 7:52 PM // reply »
7,572 Comments

@Patrick,

Anytime. Good luck!


Post Comment  |  Ask Ben

Recent Blog Comments
Mar 21, 2010 at 8:57 PM
The Bourne Ultimatum Starring Matt Damon And Julia Stiles
late to the party, but my observation is this: rewatch carefully for the platonic nature of the relationship between nicki and jason. she never flirts with him. he never comes on to her. they alway ... read »
Mar 21, 2010 at 7:40 PM
Is Simulating User-Input Events With jQuery Ever A Good Idea?
A couple of things. One you embed the initial state of of more-info in the CSS. IMHO, that behavior should be in jQuery: moreInfo.hide(); It shows that the behavior your toggling and closing is mor ... read »
Mar 21, 2010 at 3:59 PM
Exploring ColdFusion Component Runtime Class Properties And Serialization
@Elliott, according to Ben's experiment, serializeJSON() doesn't access the private data by default - it doesn't even access the getHair() method - so trying to clone a Girl.cfc via serializeJSON/des ... read »
Mar 21, 2010 at 3:49 PM
Ask Ben: Javascript String Replace Method
I'm confused a bit by what you are asking, but if had this sentence: The color, red, is in the style statement; style: red;. and wanted to remove all or change all of the commas, colons, and semi-c ... read »
Mar 21, 2010 at 3:13 PM
Ask Ben: Javascript String Replace Method
I am trying to make a java program to count the number of times that these punctuation marks occur in a body of text: , : ; . ! - ' " ? / \ I am using this piece to ferret out the commas: numcommas ... read »
Mar 21, 2010 at 11:13 AM
A New Wrist Pain
@chiropractor suwanee, Spoken like someone trying to sell something. Other than for minor, temporary relief from some back pain, chiropractic treatment is nothing but placebo effect and quackery. ... read »
Mar 21, 2010 at 6:32 AM
ColdFusion CFPOP - My First Look
Apologies... The field name in the db for C. is "BounceCode" It stores the code / message which is returned in the email. Sorry for the confusion. ... read »
Mar 21, 2010 at 6:29 AM
ColdFusion CFPOP - My First Look
@Jose Galdamez, Hi Ben and Jose 1st of all.. big thanks to Jose for his Skype chat a few weeks back. Your time was much appreciated. I have come up with a rather unelegant solution to my problem a ... read »