PHP Usort

Recently my colleague at work found a unique need to use the PHP usort function. He wanted to sort a two dimensional array with one function, and be able to easily change the sort order for each element depending on how the customer wanted to see the data in a report. Since I've never found a use for usort() before, I was interested in figuring out how to use it effectively.

Previously, I've covered sorting arrays in my article on "PHP Sorting while Maintaining Key and Value Relations."  In short, PHP has functions for sorting arrays by the associative key values, or the array elements in ascending or descending order. Or if you don't care about the key to element relationship you can resort and assign new key values. I covered these functions in my article on "PHP Sorting and Renumbering Array Keys."

But in both of these articles, I conveniently left out usort.  The usort() function allows you to sort arrays by using a comparison function. Let's take a look. The proper syntax for usort:

usort(the array you want to sort, "the comparison function");

The array will be the sorted with new key values assigned to the array elements. The results of usort is the same array variable, re-ordered, and re-keyed. Usort works by looping through the array comparing two elements at a time until the array is completely sorted.

Obviously, to use usort you'll need to write a comparison function to compare each set of two elements.  That's no big deal.  The return from the comparison function used by usort is restricted to one of three integer values: -1, 0, or 1.  For simplicity, let's call the two elements that the comparison function will compare from the array $a and $b.  We'll write a simple comparison function.

        function usortCompare($a, $b)
        {
            if ($a == $b) return 0;
            return ($a > $b) ? -1 : 1;
        }

If the two elements are equal then their sort order is undefined, and they'll both be placed one right after the other in the sorted array.

Let's take a look at two array examples. First, a fruits array showing the before and after the usort.

$fruits = array("d"=>"lemon", "a"=>"orange", "b"=>"banana", "c" => "cantalope" , "e" => "apple", "f" => "apple");
usort($fruits, "usortCompare");

Here's the before

fruits1-620

And here's the after usort

fruits2-620

Let's do this with mixed numbers and letters to check for mixed arrays and how numbers are sorted

$mixalpha = array(5, 6, 8 , 12 , 15, 3, 1, 9, '7x', '7f', '7a');
usort($mixalpha, "usortCompare");

Here's the before.

mixalpha1-620

And here's the after usort.

mixaplha2-620

In both examples, you can see the key values after the usort have been reassigned. The arrays are ordered alphabetically, because of the way we assigned the values in the comparison function. The numbers are sorted in natural order, instead of 1, 12, 15, 3, 5... you get 1, 3, 5, 12, 15.. One of the benefits of usort is its natural order sorting.  Also, the comparison function can be called with, or without parameters. We'll use parameters in my next example.

OK, seems pretty straight forward, let's get fancy, and sort a multidimensional array.  Typically you'll get a multidimensional array when you get results back from a database. Every row from the database will have multiple columns returned that you'll want to sort for reports. Yes, I know you can do this with SQL, but sometimes changing queries may not be convenient in your application.

Let's set up a multidimensional array for testing.

$testArray['pontiac'] = array('id' => '54321', 'name' => 'chopped liver', 'modify_date' => '2013-12-04', 'color' => 'red'); 
$testArray['buick'] = array('id' => '12345', 'name' => 'chopped liver', 'modify_date' => '2013-04-14', 'color' => 'green'); 
$testArray['chevy'] = array('id' => '32154', 'name' => 'turkey call', 'modify_date' => '2010-05-11', 'color' => 'red'); 
$testArray['geo'] = array('id' => '21543', 'name' => 'rolling river', 'modify_date' => '2013-05-11', 'color' => 'purple'); 
$testArray['hummer'] = array('id' => '43215', 'name' => 'syncapated steel', 'modify_date' => '2014-12-04', 'color' => 'red');
$testArray['hummer'] = array('id' => '43512', 'name' => 'chopped liver', 'modify_date' => '2014-12-04', 'color' => 'red');

First, a shameless plug, let's look at the array with my newchk variable checker, available free. There's a link to the code in the left sidebar menu.

testarr1-620

This array might be something you would get directly out of the database. I put car associative keys on the array just to show you the re-keying in usort again.

The goal here is to sort the various elements of the array depending on a primary field and then a secondary field, a multidimensional sort. As a bonus will throw in a case sensitivity option, and a ascending or descending ordering option.

Let's write the code with lots of comments:

/*
 *  This function sets up usort to sort a multiple dimmensional array, 
 *  in asc or desc order, and case sensitive or not
 */
function multiKeySort(&$testArray, $sortFields, $reverse=false, $ignorecase=false) 
{
	// we want to make sure that field(s) we want to sort are in an array for the compare function
	if (!is_array($sortFields)) $sortFields = array($sortFields);

    // our usort function that does all the work
	// notice the parameters
	usort($testArray, sortcompare($sortFields, $reverse, $ignorecase));
}

/*
 * This is the usort compare function
 * It is preset to sort from asc and to not ignore case
 * 
 * @return  bool  We only return a 1 , -1, or 0, which is what usort expects
 */
function sortcompare($sortFields, $reverse=false, $ignorecase=false) 
{    
    return function($a, $b) use ($sortFields, $reverse, $ignorecase) 
    {
	$cnt=0;
	// check each sort field in the order specified
	foreach($sortFields as $aField) {
            // check the value for ignorecase
            $ignore = is_array($ignorecase) ? $ignorecase[$cnt] : $ignorecase;
            // Determines whether to sort with or without case sensitive
            $result = $ignore ? strnatcasecmp ($a[$aField], $b[$aField]) : strnatcmp($a[$aField], $b[$aField]);
            // the $result will be 1, -1, or 0  

            // check to see if you want to reverse the sort order
            // to reverse the sort order you simply flip the return value by multplying the result by -1
            $revcmp = is_array($reverse) ? $reverse[$cnt] : $reverse;
            $result = $revcmp ? ($result * -1) : $result;

            // the first key that results in a non-zero comparison determines the order of the elements
            if ($result != 0) break;
            $cnt++;
	}
    //returns 1, -1, or 0
	return $result;
    };
} // end sortcompare

Remember, usort only expects a 1, -1, or 0 returned from its comparison function, and that's all our sortcompare function will return.  It turns out the call by reference ampersand in the multiKeySort function, &$testArray, is needed for this to work properly.

We have our $testArray and our code, lets sort the $testArray.  Here's the code that does the sort.  You'll notice that we pass parameters to the multiKeySort that implements the usort function.

echo "Sort by color, then date (reversed)";

multiKeySort($testArray, array('color', 'modify_date'),array(false,true));

The "array('color', 'modify_date')" is the $sortFields parameter used in the multiKeySort function. Here's the results.

testarr2-620

You'll notice I put three red colors in the secondary array, they all come at the bottom sorted by the secondary modify_date field, so the code works.  The associative "car" keys have gone away as a result of the usort.

Let's do another sort, sorting by name and then modify_date.

        
	echo "Sort by name (reversed) & then date_modified: ";

	multiKeySort($testArray, array('name','modify_date'),array(true,false));

And the result.

testarr3-620

I have three "chopped liver" names, they are together sorted by modify_date.

One more example, sorting by name and modify_date again, only this time we will reverse the sort order, so "chopped liver" will come at the top, and the dates will be descending, instead of ascending.  To do this we switch the true, false in the second array in the call to multiKeySort.

echo "Sort by name & then date_modified(reversed): ";

multiKeySort($testArray, array('name','modify_date'),array(false,true));

And the result

testarr4-620

In closing, I want to thank my colleague at work, Bob Caverly, for giving me the opportunity to become better acquainted with benefits of using usort().