David
David

Reputation: 495

Sort a flat, associative array by numeric values, then by keys

I have an array of string keys with numeric values to be used to create a list of tags with the number of occurrences of each tag resembling this:

$arrTags = [
    'mango' => 2, 
    'orange' => 4, 
    'apple' => 2,
    'banana' => 3
];

I want to display the tags in a list with descending values, then the tag names ascending to produce:

orange (4)  
banana (3) 
apple (2) 
mango (2)

arsort() is not suitable because it will put mango before apple. I'm guessing that usort() may be the way, but I'm not finding a suitable example in the comments on php.net.

Upvotes: 31

Views: 38586

Answers (8)

Sijmen Ruwhof
Sijmen Ruwhof

Reputation: 61

The previous proposed solution seems logical, but it just doesn't work (on PHP versions under 7):

ksort($arrTags);
arsort($arrTags);

The complete PHP code to realize the asked sorting, will be:

$k = array_keys($arrTags);
$v = array_values($arrTags);
array_multisort($k, SORT_ASC, $v, SORT_DESC);
$arrTags = array_combine($k, $v);

Please note that array_multisort() uses references on user input, so you'll have to use two temporary variabels ($k and $v) to supply content as user input. This way array_multisort() can change the content. Later on, rebuild the sorted array via array_combine().

I've built a reusable function to accomplish this task:

<?php
/**
 * Sort a multi-dimensional array by key, then by value.
 *
 * @param array Array to be sorted
 * @param int One of the available sort options: SORT_ASC, SORT_DESC, SORT_REGULAR, SORT_NUMERIC, SORT_STRING
 * @param int One of the available sort options: SORT_ASC, SORT_DESC, SORT_REGULAR, SORT_NUMERIC, SORT_STRING
 * @return void
 * @example The following array will be reordered:
 *  $a = array(
 *      'd' => 4,
 *      'c' => 2,
 *      'a' => 3,
 *      'b' => 1,
 *      'e' => 2,
 *      'g' => 2,
 *      'f' => 2,
 *  );
 *  SortArrayByKeyThanValue($a);        # reorder array to: array(
 *      'b' => 1,
 *      'c' => 2,
 *      'e' => 2,
 *      'f' => 2,
 *      'g' => 2,
 *      'a' => 3,
 *      'd' => 4,
 *  );
 * @author Sijmen Ruwhof <sijmen(a)secundity.com>
 * @copyright 2011, Secundity
 */
function SortArrayByKeyThanValue (&$pArray, $pSortMethodForKey = SORT_ASC, $pSortMethodForValue = SORT_ASC)
{
    # check user input: sorting is not necessary
    if (count($pArray) < 2)
        return;

    # define $k and $v as array_multisort() needs real variables, as user input is put by reference
    $k = array_keys  ($pArray);
    $v = array_values($pArray);

    array_multisort(
        $v, $pSortMethodForValue,
        $k, $pSortMethodForKey
    );
    $pArray = array_combine($k, $v);
}
?>

Upvotes: 6

mickmackusa
mickmackusa

Reputation: 47894

Use uksort() to pass the keys into the custom function's scope; within that scope, access the associated value by using the key on the passed in (full) array.

The advantage of this is the time complexity -- this will be more direct than two separate sorting function calls and doesn't require the setup of array_multisort(). Also, array_multisort() will destroy numeric keys (not that the asker's keys are numeric) https://3v4l.org/rQak4.

Although the spaceship (3-way) operator was not available back when this question was asked, it is now and it makes the comparison much easier/cleaner now.

Below will sort by values DESC ($b <=> $a), then keys ASC ($array[$a] <=> $array[$b]) and preserve the keys. Whenever you wish to have ASC sorting write the $a values on the left of the comparison operator and $b on the right. Conversely, for DESC sorting, write the $b value on the left and the $a value on the right. The arrays on either side of the comparison operator will be compared by their count (assumed to be the same) then have their elements individually compared from from left-to-right until there is no tie to break.

From PHP7.4 and up, the syntax is very concise. (Demo)

uksort($arrTags, fn($a, $b) => [$arrTags[$b], $a] <=> [$arrTags[$a], $b]);

From PHP7.0 - PHP7.3, you must pass in the main array with use(). (Demo)

uksort(
    $arrTags,
    function($a, $b) use ($arrTags) {
        return [$arrTags[$b], $a] <=> [$arrTags[$a], $b];
    }
);

Upvotes: 1

Tatu Ulmanen
Tatu Ulmanen

Reputation: 124768

You're thinking too complicated:

ksort($arrTags);
arsort($arrTags);

Now your array is sorted like you want it to.

Note: This technique is only reliable in PHP7 and up: https://3v4l.org/ma7ab

Upvotes: 0

Sid
Sid

Reputation: 4502

//preserve arrays keys for later use
$ar1= array_keys($your_array);

//preserve array's values for later use
$ar2= array_values($your_array);

//perform sorting by value and then by key
array_multisort($ar2, SORT_DESC, $ar1, SORT_DESC);

//combine sorted values and keys arrays to new array
$sorted_array = array_combine($ar1, $ar2);

Must be ok.

Upvotes: 1

SlappyTheFish
SlappyTheFish

Reputation: 2384

Have a look at examples #3: http://php.net/manual/en/function.array-multisort.php

You'll need to create two arrays to use as indexes; one made up of the original array's keys and the other of the original array's values.

Then use multisort to sort by text values (keys of the original array) and then by the numeric values (values of the original array).

Upvotes: 16

David
David

Reputation: 495

SOLVED

After a little experimentation I discovered that array_multisort does the trick nicely:

$tag = array(); 
$num = array();

foreach($arrTags as $key => $value){ 
$tag[] = $key; 
$num[] = $value; 
}

array_multisort($num, SORT_DESC, $tag, SORT_ASC, $arrTags);

:)

Upvotes: 15

Jon Bernhardt
Jon Bernhardt

Reputation: 551

As Scott Saunders hints in his comment to David's solution, you can use the array_keys() and array_values() functions to get rid of the loop. In fact, you can solve this in one line of code:

array_multisort(array_values($arrTags), SORT_DESC, array_keys($arrTags), SORT_ASC, $arrTags);

Upvotes: 44

woodchucky
woodchucky

Reputation: 211

SlappyTheFish is correct re: using array_multisort vs. ksort, arsort

In David's example ksort, arsort works fine, however if the keys' string values contain characters other than alphabetic characters, the sort may not work as intended.

ex:

$arrTags['banana'] = 3;
$arrTags['mango'] = 2;
$arrTags['apple1'] = 2;
$arrTags['orange'] = 4;
$arrTags['almond1'] = 2;

ksort($arrTags);
arsort($arrTags);

print_r($arrTags);

returns:

Array
(
    [orange] => 4
    [banana] => 3
    [apple1] => 2
    [mango] => 2
    [almond1] => 2
)

however using:

$arrTags['banana'] = 3;
$arrTags['mango'] = 2;
$arrTags['apple1'] = 2;
$arrTags['orange'] = 4;
$arrTags['almond1'] = 2;

$tag = array();
$num = array();

foreach($arrTags as $key => $value){
    $tag[] = $key;
    $num[] = $value;
}

array_multisort($num, SORT_DESC, $tag, SORT_ASC, $arrTags);


print_r($arrTags);

returns:

Array
(
    [orange] => 4
    [banana] => 3
    [almond1] => 2
    [apple1] => 2
    [mango] => 2
)

Upvotes: 3

Related Questions