Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney

Sorting Arrays Using Tiered Comparisons In ColdFusion

By
Published in

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 than b.p1, return -1.
    • If a.p1 is greater than b.p1, return 1.
    • If a.p1 is equal to b.p1, compare p2.
  • Compare p2:
    • If a.p2 is less than b.p2, return -1.
    • If a.p2 is greater than b.p2, return 1.
    • If a.p2 is equal to b.p2, compare p3.
  • Compare p3:
    • If a.p3 is less than b.p3, return -1.
    • If a.p3 is greater than b.p3, return 1.
    • If a.p3 is equal to b.p3, return 0.

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# &rarr;
					<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:

A table showing all three properties sorting ascending.

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

A table showing the p1 property sorting descending.

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

A table showing the p1 and p2 properties sorting descending.

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

A table showing all three properties sorting descending.

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel