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

Comments (2)  |  Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page




Reader Comments

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

Posted by Rob Pilic on Dec 7, 2007 at 5:17 PM


@Rob,

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

Posted by Ben Nadel on Dec 10, 2007 at 8:01 AM


Post Comment  |  Ask Ben


Home   |   Web Log   |   ColdFusion   |   Projects   |   Resume   |   Job Form   |   Search   |   Contact
Epicenter Consulting - Custom Software Solutions for Business Evolution HostMySite.com - The Leader In ColdFusion Hosting