Sorting ColdFusion Arrays With Sortable Interfaces

Posted November 7, 2006 at 8:01 AM by Ben Nadel

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

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 »
10,640 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 »
10,640 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 »
5 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 »
10,640 Comments

@Patrick,

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


Mar 1, 2010 at 9:32 AM // reply »
5 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 »
10,640 Comments

@Patrick,

Anytime. Good luck!


Apr 6, 2010 at 12:35 PM // reply »
1 Comments

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


Apr 7, 2010 at 9:35 PM // reply »
10,640 Comments

@Khurram,

Glad you found this helpful. Please feel free to post any reference you want to this code.


Jun 8, 2010 at 12:06 PM // reply »
2 Comments

@anybody with an answer

How does this work with sorting by alpha?


Jun 8, 2010 at 12:22 PM // reply »
2 Comments

how well does this work with alpha shorting?


Jun 8, 2010 at 1:36 PM // reply »
10,640 Comments

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



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 10, 2012 at 7:21 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
Update! Instead of $(eval(options.insertAfter)).after(data['insertData']); I now use: var ajaxNode = document.createElement('span'); var parent = $(eval(options.insertAfter))[0].parentNode; ... read »
Feb 10, 2012 at 6:18 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API ne ... read »
Feb 10, 2012 at 11:30 AM
Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js
After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/ ... read »
JM
Feb 10, 2012 at 9:10 AM
My Safari Browser SQLite Database Hello World Example
@Amy, Here is a very good tutorial on how to use JOIN: http://www.sqltutorial.org/sqljoin-innerjoin.aspx ... read »
Feb 10, 2012 at 4:42 AM
Building A Twitter-Inspired RESTful API Architecture In ColdFusion
This is great, very useful Ben. I spotted a small typo in the api.cgm listing: <cfthrow type="Unauthroized" /> Cheers Stefan ... read »
Feb 9, 2012 at 10:35 PM
CFDirectory Filtering Uses Pipe Character For Multiple Filters (Thanks Steve Withington)
I was wondering if there would be a filter you could apply so that you got everything but what you included in the filter. As in show me all docs that are not a .pdf. ... read »
Feb 9, 2012 at 10:29 PM
Learning ColdFusion 9: Application-Specific Data Sources
@Ben, No offence, but if people were really wanting advanced features they would be using a platform like ASP.NET MVC. CFML is so structurally compromised as a tag-based scripting language that ... read »
Feb 9, 2012 at 10:03 PM
Subversion - Cleanup Failed To Process The Following Paths
@Leviaguirre, do you still have problems with this? ... read »