29

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.

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
David
  • 475
  • 1
  • 5
  • 8

8 Answers8

42

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);
Jon Bernhardt
  • 531
  • 1
  • 4
  • 4
  • Thanks for this code. I have the exact situation as the original question and this works for me. What's bothering me is that I'm not exactly sure what's happening here. What is the function of the last parameter? I can see that the last parameter (being passed by reference) is the only possibility for returning the changed array, OK. But how are the array sets related to one another? When multisort does its work going left to right are the subsequent arrays sorted along with them. If so then all arrays must have the same length which is not required by the documentation. #confused – andypotter Feb 18 '14 at 14:25
  • @andypotter: Basically, `array_multi_sort()` will internally create another array `$indirect` where each index is an array containing all the values of the provided arrays: `$indirect[$i] = [$v1, $v2, $v3, ..., NULL]`. Then QuickSort is applied on the array using a special comparing function which will first compare `$indirect[$a][$r]` to `$indirect[$b][$r]` where `$r==0` if they are equals, `$r` is increased until `$indirect[$a][$r]` is `null` (all provided arrays have been used). Finally each array is rewritten according to `$indirect`. If you use different sized arrays, FALSE is returned. – 2072 Jun 26 '14 at 20:05
  • @dearsina: Good point. However the OP specifically referred to string keys. In fact he asked that "any tags that have the same numeric value... be sorted alphabetically". – Jon Bernhardt Dec 20 '18 at 14:10
15

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).

Scopey
  • 6,269
  • 1
  • 22
  • 34
SlappyTheFish
  • 2,344
  • 3
  • 34
  • 41
15

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);

:)

NullPoiиteя
  • 56,591
  • 22
  • 125
  • 143
David
  • 475
  • 1
  • 5
  • 8
  • 2
    That would be SlappyTheFish's answer, right? I think you should mark that answer as accepted and then look at the array_keys() and array_values() functions to get rid of your loop. – Scott Saunders Feb 17 '10 at 18:53
  • 4
    Also a note for future visitors: `$tag = array_keys( $arrTags); $num = array_values( $arrTags);` would form the identical arrays without a loop. – nickb Apr 08 '13 at 20:27
  • Why this is not working for me? I copy the above sample and solution just got same result in the question. – Tim Yao Dec 01 '14 at 03:06
  • @TimYao could it be that your keys are numerical? https://3v4l.org/uUYUO – dearsina Dec 19 '18 at 06:44
6

The previous proposed solution seems logical, but it just doens't work:

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);
}
?>
Sijmen Ruwhof
  • 61
  • 1
  • 2
  • This answer is provably incorrect: https://3v4l.org/NsbqN See how it is not sorting by the values first? This answer is effectively the same sorting as `ksort()`. – mickmackusa Dec 15 '20 at 22:21
3

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
)
woodchucky
  • 211
  • 2
  • 2
  • This answer is misleading and no longer current. From PHP7 and higher, `ksort() then arsort()` will correctly sort the array. It is the sorting algorithm prior to PHP7 that made the technique unreliable. Importantly, this differing behavior has nothing to do with numbers existing in the keys. See this demo: https://3v4l.org/d3a1m Please update your answer to be correct and current. At the moment, the only part of your answer that it correct/true is copied from https://stackoverflow.com/a/2282247/2943403 – mickmackusa Dec 15 '20 at 22:32
  • Your latter snippet could have been more concise: https://3v4l.org/8UcaZ or even more concisely, don't use a loop: https://3v4l.org/pXl20 – mickmackusa Sep 22 '22 at 06:10
1
//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.

Sid
  • 4,302
  • 3
  • 26
  • 27
1

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.

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];
    }
);
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
0

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

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Tatu Ulmanen
  • 123,288
  • 34
  • 187
  • 185
  • I was just about to post this exact answer. Anyway, tried it and I can confirm it works. – Michael Mior Feb 17 '10 at 15:54
  • 4
    php sorts aren't stable, so you aren't guaranteed this will work. http://www.php.net/manual/en/array.sorting.php – goat Feb 17 '10 at 16:12
  • @chris, that's true but I still haven't found any cases where it would not work, so I'd go on with this. – Tatu Ulmanen Feb 17 '10 at 16:31
  • 5
    thanks for the idea but it doesn't work for me. it's sorted the numbers descending fine but the key sorting for the same numerical values is pretty random, certainly not alphabetical. not sure why though. – David Feb 17 '10 at 18:35