1

I have the following array:

$array = [
    'z' => 2,
    'd' => 1,
    'a' => 2,
];

Now I would like to sort by the value (integer) and then sort by the key based on whether it is in this allowlist:

$allowlist = ['a', 'd'];

So I did the following:

arsort($array);
uksort($array, function($a, $b) {
    return in_array($a, $allowlist) ? -1 : 1;
});

But that returns:

[d]: 1
[a]: 2
[z]: 2

What I really want is to sort the values first and then if there is a tie breaker, sort the key based on whether its in that allowlist which should result in this:

[a]: 2
[z]: 2
[d]: 1
ashleedawg
  • 20,365
  • 9
  • 72
  • 105
strangeQuirks
  • 4,761
  • 9
  • 40
  • 67
  • I think the sorts should keep the current order in case of a tie, so maybe doing the `uksort` first and then the `arsort` solves the problem. The sort that is more important should come last. – Ali Rahimi Jul 22 '21 at 11:13
  • "I think the sorts should keep the current order in case of a tie" --- if only it's stable. PHP does not guarantee (before 8.0) built-in sorts to be stable. – zerkms Jul 22 '21 at 11:15
  • Oh yes and so my solution doesn't work. – Ali Rahimi Jul 22 '21 at 11:16
  • Read the manual: https://www.php.net/manual/en/array.sorting.php – Martin Jul 22 '21 at 11:18
  • What about if you have a key of `x` with a value of `2`, how should that sort relative to `z`? They both have the same value, and neither are on the allowlist – Chris Haas Jul 22 '21 at 11:44

2 Answers2

2

You can use uksort which will give you the keys, and you can pass the $array into your sort function to get access to the value.

$array = [
    'a' => 2,
    'z' => 2,
    'd' => 1
];
$allowlist = ['a', 'd'];

uksort(
    $array,
    static function($a, $b) use ($array, $allowlist) {
        if($array[$a] === $array[$b]) {
            return in_array($a, $allowlist) ? -1 : 1;
        }
        return $array[$b] <=> $array[$a];
    }
);

This was inspired by this answer: https://stackoverflow.com/a/65315474/231316

Demo here: https://3v4l.org/toZIt

Chris Haas
  • 53,986
  • 12
  • 141
  • 274
  • @mickmackusa, the accepted answer on that page was essentially RTFM which we kind of did back then, but I no longer think is a good answer. That’s why I’m not voting it as a duplicate. Your answer had zero ups (but one now!) and does what the OP wants. For arrays of consequence, it might have performance problems, but “working” needs to come before that. Out of curiosity, how would you solve this? The two arrays method with multi sort? – Chris Haas Jul 22 '21 at 11:27
  • Personally, although I like using `fn` with auto capture, the double `in_array` with rocket is a speed bump in my brain that I have to think about for a bit. I’m not saying it is bad in anyway, in my codebases I would just need a comment to explain what is going on, whereas the one I posted is more self documenting to me. But if you post that as an answer, I’d be glad to upvote it! – Chris Haas Jul 22 '21 at 11:40
  • Consider the extensibility of this technique. If the OP ever wants to extend the sorting rules to implement another tiebreaker, then with my cascading rule syntax, there is just one more line of code and no nested `if` structure. It is a style preference, of course. – mickmackusa Jul 23 '21 at 05:57
1

I recommend making as few evaluations as possible and as few function calls as possible.

There are deliberately two spaceship operators in my snippet -- this means that the in_array() calls are only executed when necessary. The spaceship operator will process arrays of sorting rules, but this will be less efficient because all in_array() functions will be called on every iteration. Don't use this demo.

Code: (Demo)

uksort(
    $array,
    fn($a, $b) =>
        $array[$b] <=> $array[$a]                                 // sort values DESC
        ?: in_array($b, $allowlist) <=> in_array($a, $allowlist)  // sort keys with priority given to whitelisted keys
);

var_export($array);

This sorts by value descending then breaks ties by checking if in_array() descending. When sorting boolean outcomes, descending puts true before false.

If you are dealing with large array volumes, then I will urge you to avoid in_array(). Instead flip your lookup array and use isset() in your custom function.

The custom function's visible cascading rules syntax makes it easy to read and extend. If you wanted to add another tie breaker for when both or neither key is whitelisted, then simply add another line with the new rule:

?: $a <=> $b  // sort by key ascending (alphabetically)
mickmackusa
  • 43,625
  • 12
  • 83
  • 136