Sorting Arrays Using Tiered Comparisons In ColdFusion
In ColdFusion, I love working with arrays. In fact, I've moved all of my data access layers over to returning arrays-of-structs instead of query objects. But, one thing that I really miss about SQL is the effortless nature of tiered ORDER BY
sorting. With ColdFusion arrays, we have some in-built sorting functionality; however, in order to achieve a multi-property sort, I needed to build an abstraction for tiered comparisons.
Let's consider the steps one might need to go through in order to sort a set of objects based on three properties, p1
, p2
, and p3
. Given the comparison of (a,b)
:
- Compare
p1
:- If
a.p1
is less thanb.p1
, return -1. - If
a.p1
is greater thanb.p1
, return 1. - If
a.p1
is equal tob.p1
, comparep2
.
- If
- Compare
p2
:- If
a.p2
is less thanb.p2
, return -1. - If
a.p2
is greater thanb.p2
, return 1. - If
a.p2
is equal tob.p2
, comparep3
.
- If
- Compare
p3
:- If
a.p3
is less thanb.p3
, return -1. - If
a.p3
is greater thanb.p3
, return 1. - If
a.p3
is equal tob.p3
, return 0.
- If
If we can describe this more abstractly as a pattern, it would be to compare each property, in turn, until one of the comparisons results in a non-equivalence. The first non-equivalence defines the overall comparison of (a,b)
. And, if none of the properties are different, the overall comparison of (a,b)
is an equivalence.
On the ColdFusion side, one way to implement this would be to define each comparison using an operator function (one that returns -1
, 0
, or 1
). And then, iterate over each operator until we find one that returns a non-0
result.
In the ColdFusion code below, I've created a variadic user defined function (UDF), that does exactly this. It takes the target array as the first argument. And then, accepts 2...N
additional arguments, each of which is a comparison operation to be execute, in turn, when comparing two array elements.
<cfscript>
// By default, all of the sorting will be ASCending. These multipliers can then change
// the sort by multiplying be either 1 (ASC) or -1 (DESC).
param name="url.p1Dir" type="numeric" default=1;
param name="url.p2Dir" type="numeric" default=1;
param name="url.p3Dir" type="numeric" default=1;
results = [
{ p1: "A", p2: 1, p3: "2025-01-01" },
{ p1: "A", p2: 2, p3: "2025-02-01" },
{ p1: "A", p2: 2, p3: "2025-03-01" },
{ p1: "B", p2: 1, p3: "2024-01-01" },
{ p1: "B", p2: 2, p3: "2024-02-01" },
{ p1: "B", p2: 1, p3: "2024-03-01" },
{ p1: "C", p2: 2, p3: "2023-01-01" },
{ p1: "C", p2: 2, p3: "2023-02-01" },
{ p1: "C", p2: 1, p3: "2023-03-01" }
];
// We're going to sort the array on all three properties. Each property will be sorted
// using one of the following operations. Each operation will be attempted in turn.
arraySortByOperations(
results,
( a, b ) => url.p1Dir * compare( a.p1, b.p1 ),
( a, b ) => url.p2Dir * sgn( a.p2 - b.p2 ),
( a, b ) => url.p3Dir * dateCompare( a.p3, b.p3 )
);
include "./render.cfm";
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I perform an in-place sort of the given array using the provided operations.
*/
public array function arraySortByOperations(
required array collection,
required function operation
/* , operation-2 */
/* , operation-3 */
/* , operation-4 */
) {
// The above argument requires at least one operation to be defined; however, this
// is a variadic function, and will accept 1...N operations. As such, let's slice
// out the full set of operations into an array.
var operations = arraySlice( arguments, 2 );
// Perform-in place sort.
return collection.sort(
( a, b ) => {
// For each comparison, we will execute one of the comparator / operation
// functions in turn. The first one to return a non-Zero result indicating
// non-equivalence will win.
for ( var operation in operations ) {
var comparison = operation( a, b );
// If the comparison results in either 1 or -1, then we have something
// to sort on - the rest of the operations become unnecessary.
if ( comparison != 0 ) {
return comparison;
}
}
// If none of the operations resulted in a non-equivalence, then the
// overall comparison results in an equivalence.
return 0;
}
);
}
</cfscript>
In this demo, I'm passing three different comparison operations to the arraySortByOperations()
function. The first performs a text-comparison on p1
; the second performs a numeric-comparison on p2
; and the third performs a date-comparison on p3
.
When this ColdFusion page executes, I'm rendering the array to a table and providing radio boxes for sorting on each of the three properties:
<cfoutput>
<table border="1" cellpadding="10">
<tr>
<th>P1</th>
<th>P2</th>
<th>P3</th>
</tr>
<cfloop array="#results#" item="result">
<tr>
<td>#encodeForHtml( result.p1 )#</td>
<td>#encodeForHtml( result.p2 )#</td>
<td>#encodeForHtml( result.p3 )#</td>
</tr>
</cfloop>
</table>
<form oninput="this.submit()">
<fieldset>
<legend>Tiered Sorting</legend>
<cfloop from="1" to="3" index="i">
<p>
p#i# →
<label>
<input
type="radio"
name="p#i#Dir"
value="1"
<cfif ( url[ "p#i#Dir" ] == 1 )>checked</cfif>
/> ASC
</label>
<label>
<input
type="radio"
name="p#i#Dir"
value="-1"
<cfif ( url[ "p#i#Dir" ] == -1 )>checked</cfif>
/> DESC
</label>
</p>
</cfloop>
</fieldset>
</form>
</cfoutput>
By default, all three properties are sorting ascending:

Then, if we flip to sorting p1
in a descending direction:

Then, if we flip to sorting p2
in a descending direction:

And finally, if we flip to sorting p3
in a descending direction:

It's still not quite as nice as the SQL ORDER BY
clause; but, it feels like a fairly clean abstraction.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →