Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Kitt Hodsden
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Kitt Hodsden ( @kitt )

Sorting Arrays With Priority Elements In ColdFusion

By
Published in

In the companion app to my feature flags book, I have sample data for deployment environments that contains values such as "Development", "Production", and "Staging". The user can edit this sample data; but, for aesthetic reasons, I always want the collection of environments to be rendered with "Development" in the first index and "Production" in the second index, regardless of what other values the user may add to or remove from the collection. Essentially, I need a way to sort the array of environments where some of the elements are in a fixed, priority location (if they're present at all) and the rest of the elements are sorted alphabetically.

In ColdFusion, I can think of two ways to do this:

  1. Handle the priority sort while handling the alphabetical sort (ie, in the same comparator function).

  2. Handle the priority sort after handling the alphabetical sort (ie, breaking the workflow up into two different steps).

I'm sure that there are other ways to do this; but, these are the two that come to mind. As always, if you have any suggestions, please drop a comment down below.

Sorting With a Single Comparator Function

When sorting arrays in ColdFusion (and most other languages), you have the option to pass a comparator function to the .sort() method. This comparator function takes two arguments and expects a signed integer to be returned. If you return a -1 (or any negative integer), the first argument is sorted towards the head of the array. If you return a 1 (or any positive integer), the second argument is sorted towards the head of the array. If you return a 0, the given arguments remain in place.

Note: In some cases, returning a value larger than an Int can cause an error. In such cases, you can use the sgn() function to clamp the comparator response.

When building a comparator function for our algorithm, we essentially have to code it in two parts: the first part deals with the sorting priority values using hard-coded offsets; and, the second part falls-back to sorting elements alphabetically.

If we can pre-calculate the index of each priority item, we can use simple maths to drive our comparator result. For example, imagine that one of the comparator invocations is as follows:

comparator( "Development", "Production" )

We know that Development needs to end up in index 1 and Production needs to end up in index 2. In that case, we can return the subtraction of the first argument index from the second argument index:

return ( 1 - 2 )

... in order to get -1, which indicates that the first argument (Development) should be sorted towards the head of the array.

Now imagine that one of our comparator invocations is reversed:

comparator( "Production", "Development" )

This time, our return expression maths would be flipped:

return ( 2 - 1 )

Which would give us 1, which indicates that the second argument (Development) is the one that should be sorted towards the head of the array.

Here's the Lucee CFML code that illustrates this compound operator. Note that the comparator function first looks at the priority sort and then falls-back to using compareNoCase() for alphabetical sorting.

<cfscript>

	environments = [
		"Integration",
		"Testing",
		"Production",
		"Staging",
		"Alpha",
		"Beta",
		"Load Testing",
		"Early Access",
		"Development"
	];

	// We always want the following values to be at the head of the sorted array, in the
	// given order, regardless of where they fall alphabetically within the greater list.
	// The rest of the elements should be sorted alphabetically, but only AFTER these
	// values have been sorted to the top.
	// --
	// Note: We're converting the array into a struct so we can quickly look up the
	// required index for each designated value.
	leadersIndex = reflectIndices( [ "Development", "Staging", "Production" ] );

	environments.sort(
		( a, b ) => {

			// First, we need to check to see if we're dealing with "leader" values that
			// need to be sorted to the top.
			var indexA = ( leadersIndex[ a ] ?: 0 );
			var indexB = ( leadersIndex[ b ] ?: 0 );

			// If both values are in the leaders collection, sort them relative to each
			// other based on their respective leader indices (ie, not alphabetically).
			if ( indexA && indexB ) {

				return ( indexA - indexB );

			// If only the first value is in the leaders collection, sort it UP.
			} else if ( indexA ) {

				return -1;

			// If only the second value is in the leaders collection, sort it UP.
			} else if ( indexB ) {

				return 1;

			}

			// If NEITHER value is in the leaders collection, sort the values using their
			// relative alphabetical sort.
			return compareNoCase( a, b );

		}
	);

	dump(
		label = "In-Sort logic",
		var = environments
	);

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I take the array of simple values and converts it into a struct of [value:index]
	* pairs. In other words, the array:
	* 
	* [ "foo", "bar", "baz" ]
	* 
	* ... becomes the structure:
	* 
	* { "foo":1, "bar":2, "baz":3 }
	*/
	private struct function reflectIndices( required array values ) {

		var index = {};

		values.each(
			( value, i ) => {

				index[ value ] = i;

			}
		);

		return index;

	}

</cfscript>

I'll show a screenshot of this down below.

Sorting With a Multi-Step Workflow

In the second priority sorting algorithm, I'm going to perform the sort in two steps. In the first step, I'll perform a native alphabetical sort. This will sort the priority elements into the wrong indices; but, it will sort everything else into the correct place.

Then, I'll loop backwards over the priority items and manually move them to the head of the array.

<cfscript>

	environments = [
		"Integration",
		"Testing",
		"Production",
		"Staging",
		"Alpha",
		"Beta",
		"Load Testing",
		"Early Access",
		"Development"
	];

	// We always want the following values to be at the head of the sorted array, in the
	// given order, regardless of where they fall alphabetically within the greater list.
	// The rest of the elements should be sorted alphabetically, but only AFTER these
	// values have been sorted to the top.
	leaders = [ "Development", "Staging", "Production" ];

	// First, we'll sort the array without any worry of leaders.
	environments.sort( "textnocase" );

	// Second, we'll loop BACKWARDS over the leaders array and move all matching leaders
	// to the front of the collection. We have to loop backwards since we're prepending
	// values to the array. If t we looped forwards, we'd add the leaders in reverse
	// order (with the last-leader becoming the first element in the final array).
	for ( leader in leaders.reverse() ) {

		leaderIndex = environments.findNoCase( leader );

		// If the leader is found in the collection, move it to the front (and delete it
		// from where it was alpha-sorted).
		if ( leaderIndex ) {

			environments.prepend( environments[ leaderIndex ] );
			environments.deleteAt( leaderIndex + 1 );

		}

	}

	dump(
		label = "Post-Sort logic",
		var = environments
	);

</cfscript>

As you can see, once we have the alpha-sorted array, we're duplicating the priority items onto the head of the collection before deleting them from their original location.

If we run both of these ColdFusion algorithms side-by-side, we get the same output:

Two arrays side by side. Each array has Development, Staging, and Production in the first three indices, respectively.

As you can see, in both algorithms, we end up with Development, Staging, and Production in the first three indices, respectively. These correspond to the order of elements within the priority sort. The rest of the array is then sorted alphabetically.

Who knew that sorting arrays could be so much fun in ColdFusion!

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