Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (May. 2008) with:

Sorting ColdFusion Arrays With Sortable Interfaces

By Ben Nadel on
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

  • <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

  • <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:

  • ((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:

  • <!---- 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:

  • <!--- 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.



Reader 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#">

@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.

@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.

@Ben

The component is just great in the way what I was looking for. It just did the job what I was willing to do.

here is what I did in the Child Class to sort on Two numeric fields Week and Year. and it did work.

<cfreturn (
VARIABLES.Instance.Target.OtherInfo.Year & VARIABLES.Instance.Target.OtherInfo.Week
GT
ARGUMENTS.Comparable.OtherInfo.Year & ARGUMENTS.Comparable.OtherInfo.Week
) />

I also want to put this code on my blog http://resource.bells.pk/ along with your reference. Would you allow me for that?

Any ways.
Thanks
Regards.

@David,

It would work the same way - that's beauty of using a sorting interface with objects - you can put whatever logic you want into the sorting methods.