7

I'm looking to be able to sort an array of associative 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 a set of data that is similar to a db query's result set, but it doesn't actually come from one so I need to sort it in PHP rather than SQL.

[
    ['first_name' => 'Homer', 'last_name' => 'Simpson', 'city' => 'Springfield', 'state' => 'Unknown', 'zip' => '66735'],
    ['first_name' => 'Patty', 'last_name' => 'Bouvier', 'city' => 'Scottsdale', 'state' => 'Arizona', 'zip' => '85250'],
    ['first_name' => 'Moe', 'last_name' => 'Szyslak', 'city' => 'Scottsdale', 'state' => 'Arizona', 'zip' => '85255'],
    ['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...

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
J Cobb
  • 294
  • 3
  • 14

5 Answers5

4

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)
port-zero
  • 657
  • 3
  • 7
4

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.

Andrew Ensley
  • 11,611
  • 16
  • 61
  • 73
  • I disagree on the reinventing the wheel. As far as I can tell, if I want to sort the data, like a data table, where I sort by one column, and the sort the data by another, `array_multisort()` is NOT the right choice, as it sorts each array independently. I need them to be sorted in a specific order, specific direction, which I do not know first hand. crazyj's solution is the one for this case. – Janis Peisenieks May 07 '14 at 11:08
1

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;
        }
    };
}
J Cobb
  • 294
  • 3
  • 14
  • I agree on not having one "right" answer, but darn it, this comes pretty close to my needs as well, with the exception, that i need to sort an indexed array, not an associative one. – Janis Peisenieks May 07 '14 at 11:02
  • I'm getting an "Warning: usort(): Array was modified by the user comparison function", using this code. I have found the following explanation, will comment when I can fix it: http://stackoverflow.com/questions/3235387/usort-array-was-modified-by-the-user-comparison-function – vicenteherrera Dec 30 '15 at 13:05
1

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.

jacobangel
  • 6,896
  • 2
  • 34
  • 35
0

I've seen a number of Stack Overflow solutions using array_multisort() inside of call_user_function_array(), but from PHP5.6 the splat operator can un-nest the sorting function and allow it be called exclusively.

Assuming your data is declared as $array:

$array = [
    ['first_name' => 'Homer', 'last_name' => 'Simpson', 'city' => 'Springfield', 'state' => 'Unknown', 'zip' => '66735'],
    ['first_name' => 'Patty', 'last_name' => 'Bouvier', 'city' => 'Scottsdale', 'state' => 'Arizona', 'zip' => '85250'],
    ['first_name' => 'Moe', 'last_name' => 'Szyslak', 'city' => 'Scottsdale', 'state' => 'Arizona', 'zip' => '85255'],
    ['first_name' => 'Nick', 'last_name' => 'Riviera', 'city' => 'Scottsdale', 'state' => 'Arizona', 'zip' => '85255'],
];

My snippet will correctly sort your data using an array of indeterminate number of rules. It permits a solitary string which states which column should be sorted ASC, or you can explicitly state the sorting flags. It is also built to allow the numeric reference of columns in case you don't know the exact column names in advance.

$sortingRules = [
    'city',                       // sort by city column ASC (could have also been: ['city'])
    ['zip', 'desc', 'numeric'],   // then sort by zip column DESC treating as numbers
    [1, 'desc'],                  // then sort by last_name column DESC
];

I have created two function declarations, but if your project never expects to deal with missing/invalid column keys being passed in, then you can eliminate the sanitizeColumnReference() function and just use $column = array_column($input, array_shift($rule));. In fact, the snippet can be roughly half its size if you remove all of the exception throwing logic.

TLDR; The beauty of this technique is that all you need to do is pass a flat array of columns and flags followed by the input array (made modifiable by reference) to array_multisort() with the aid of the "splat operator" (...) and all the work is done.

function sanitizeColumnReference($row, $columnReference) {
    if (!isset($row[$columnReference]) && is_int($columnReference)) {
        $columnReference = array_keys($row)[$columnReference] ?? null;  // attempt to derive column name by position
        if ($columnReference === null) {
            throw new Exception('Failed to locate column by position using column reference: ' . $columnReference);
        }
    }
    return $columnReference;
}

function dynamicSort(&$input, $sortingRules) {
    if (!$input || !$sortingRules || !is_array($input) || !is_array($sortingRules)) {
        return;  // return silently
    }
    $firstRow = current($input);
    $sortingParams = [];
    foreach ($sortingRules as $rule) {
        $rule = (array)$rule; // permit the passing of a solitary string as a sorting rule
        $columnReference = sanitizeColumnReference($firstRow, array_shift($rule)); 
        $column = array_column($input, $columnReference);
        if (!$column) {
            throw new Exception('Failed to source sortable data from column reference: ' . $columnReference);
        }
        $sortingParams[] = $column;
        foreach ($rule as $flag) {
            $sortingParams[] = constant('SORT_' . strtoupper($flag));  // convert strings to usable CONSTANTs
        }
    }
    $sortingParams[] = &$input;
    // var_export($sortingParams);
    array_multisort(...$sortingParams); // unpack into native sorting function
}

Here's how you would call the custom/dynamic sorting function:

dynamicSort($array, $sortingRules);  // this modifies by reference like native sorting functions

Output: (Demo)

array (
  0 => 
  array (
    'first_name' => 'Moe',
    'last_name' => 'Szyslak',
    'city' => 'Scottsdale',
    'state' => 'Arizona',
    'zip' => '85255',
  ),
  1 => 
  array (
    'first_name' => 'Nick',
    'last_name' => 'Riviera',
    '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',
  ),
)
mickmackusa
  • 43,625
  • 12
  • 83
  • 136