Sort Array of MultiDiminsional Arrays on More Than One “Column” (Key) With Specified Sort Options
-
03-07-2019 - |
Question
I'm looking to be able to sort an array of multi-dimensional arrays on more than one column. To further complicate it I'd like to be able to set specific sort options per key/column. I have what is similar the result of a DB query, but doesn't actually come from one, therefore the need to sort it in PHP rather than SQL.
Array
(
[0] => Array
(
[first_name] => Homer
[last_name] => Simpson
[city] => Springfield
[state] => Unknown
[zip] => 66735
)
[1] => Array
(
[first_name] => Patty
[last_name] => Bouvier
[city] => Scottsdale
[state] => Arizona
[zip] => 85250
)
[2] => Array
(
[first_name] => Moe
[last_name] => Szyslak
[city] => Scottsdale
[state] => Arizona
[zip] => 85255
)
[3] => Array
(
[first_name] => Nick
[last_name] => Riviera
[city] => Scottsdale
[state] => Arizona
[zip] => 85255
)
)
I would like to be able to sort it similar to what could be done with a DB query. Oh, and sometimes a column/key needs to be specified by number.
What I had in mind was something similar to this:
$sortOptions = array( array( 'city', SORT_ASC, SORT_STRING ),
array( 'zip', SORT_DESC, SORT_NUMERIC),
array( 2, SORT_ASC, SORT_STRING) // 2='last_name'
);
$sorter = new MultiSort($data, $sortOptions );
$sortedData = $sorter->getSortedArray() ;
print_r( $jmsSorted);
What I would like to end up with is this:
Array
(
[0] => Array
(
[first_name] => Nick
[last_name] => Riviera
[city] => Scottsdale
[state] => Arizona
[zip] => 85255
)
[1] => Array
(
[first_name] => Moe
[last_name] => Szyslak
[city] => Scottsdale
[state] => Arizona
[zip] => 85255
)
[2] => Array
(
[first_name] => Patty
[last_name] => Bouvier
[city] => Scottsdale
[state] => Arizona
[zip] => 85250
)
[3] => Array
(
[first_name] => Homer
[last_name] => Simpson
[city] => Springfield
[state] => Unknown
[zip] => 66735
)
)
UPDATE: I think that ideally, a solution would result in dynamically creating
array_multisort( $city, SORT_ASC, SORT_STRING, $zip, SORT_DESC, SORT_NUMERIC, $last_name, SORT_ASC, SORT_STRING, $inputArray);
The problem is that I don't want to have to "hard code" those key names in there. I tried creating a solution based upon Example #3 Sorting database results from the array_multisort()
documentation that ended up using array_multisort()
but I cannot seem to find a way to use my dynamically built argument list for array_multisort()
.
My attempt was to "chain" those arguments together into an array and then
call_user_func_array( 'array_multisort', $functionArgs);
That results in an
Warning: Parameter 2 to array_multisort() expected to be a reference, value given in...
Solution 4
Here is what I finally settled on for being able to sort multi-dimensional arrays. Both of the answers above are good but I was also looking for something flexible.
I definitely don’t think there is any one “right” answer, but this is what works for my needs and is flexible.
As you can see from my @link
in the comment of _usortByMultipleKeys()
it was adapted from a comment in the PHP manual that currently doesn't seem to exist, but I believe http://www.php.net/manual/en/function.usort.php#104398 is a new version of the original comment. I have not explored using that new suggestion.
/**
* Sort the resultSet.
*
* Usage: $sortOptions = array(
* 'section', // Defaults to SORT_ASC
* 'row' => SORT_DESC,
* 'retail_price' => SORT_ASC);
* $results->sortResults($sortOptions);
*
* @param array $sortOptions An array of sorting instructions
*/
public function sortResults(array $sortOptions)
{
usort($this->_results, $this->_usortByMultipleKeys($sortOptions));
}
/**
* Used by sortResults()
*
* @link http://www.php.net/manual/en/function.usort.php#103722
*/
protected function _usortByMultipleKeys($key, $direction=SORT_ASC)
{
$sortFlags = array(SORT_ASC, SORT_DESC);
if (!in_array($direction, $sortFlags)) {
throw new InvalidArgumentException('Sort flag only accepts SORT_ASC or SORT_DESC');
}
return function($a, $b) use ($key, $direction, $sortFlags) {
if (!is_array($key)) { //just one key and sort direction
if (!isset($a->$key) || !isset($b->$key)) {
throw new Exception('Attempting to sort on non-existent keys');
}
if ($a->$key == $b->$key) {
return 0;
}
return ($direction==SORT_ASC xor $a->$key < $b->$key) ? 1 : -1;
} else { //using multiple keys for sort and sub-sort
foreach ($key as $subKey => $subAsc) {
//array can come as 'sort_key'=>SORT_ASC|SORT_DESC or just 'sort_key', so need to detect which
if (!in_array($subAsc, $sortFlags)) {
$subKey = $subAsc;
$subAsc = $direction;
}
//just like above, except 'continue' in place of return 0
if (!isset($a->$subKey) || !isset($b->$subKey)) {
throw new Exception('Attempting to sort on non-existent keys');
}
if ($a->$subKey == $b->$subKey) {
continue;
}
return ($subAsc==SORT_ASC xor $a->$subKey < $b->$subKey) ? 1 : -1;
}
return 0;
}
};
}
OTHER TIPS
In PHP 5.3 every parameter in the array has to be a reference when calling array_multisort()
with call_user_func_array()
.
This function sorts a multidimensional array and shows a way to build an array of referenced params that works correctly.
function msort()
{
$params = func_get_args();
$array = array_pop($params);
if (!is_array($array))
return false;
$multisort_params = array();
foreach ($params as $i => $param)
{
if (is_string($param))
{
${"param_$i"} = array();
foreach ($array as $index => $row)
{
${"param_$i"}[$index] = $row[$param];
}
}
else
${"param_$i"} = $params[$i];
$multisort_params[] = &${"param_$i"};
}
$multisort_params[] = &$array;
call_user_func_array("array_multisort", $multisort_params);
return $array;
}
Example:
$data is the given array from the question
$sorted_data = msort('city', SORT_ASC, SORT_STRING, 'zip', SORT_DESC, SORT_NUMERIC, $data)
This should work for the situation you describe.
usort($arrayToSort, "sortCustom");
function sortCustom($a, $b)
{
$cityComp = strcmp($a['city'],$b['city']);
if($cityComp == 0)
{
//Cities are equal. Compare zips.
$zipComp = strcmp($a['zip'],$b['zip']);
if($zipComp == 0)
{
//Zips are equal. Compare last names.
return strcmp($a['last_name'],$b['last_name']);
}
else
{
//Zips are not equal. Return the difference.
return $zipComp;
}
}
else
{
//Cities are not equal. Return the difference.
return $cityComp;
}
}
You could condense it into one line like so:
function sortCustom($a, $b)
{
return ($cityComp = strcmp($a['city'],$b['city']) ? $cityComp : ($zipComp = strcmp($a['zip'],$b['zip']) ? $zipComp : strcmp($a['last_name'],$b['last_name'])));
}
As far as having a customizable sort function, you're reinventing the wheel. Take a look at the array_multisort()
function.
You might want to try using usort. All you have to do is make a functions that tell the sorter how to sort it. The docs have more info on how to do that.