-
Notifications
You must be signed in to change notification settings - Fork 11.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[5.6] Fixed stable sortBy() not sorting correctly when chained #21366
Conversation
Can we just revert the sortBy behavior to its old functionality? @themsaid ... would that be a problem? I don't see why there is all of a sudden a big flurry of activity around this function. |
@taylorotwell we can revert safely, I suggest we don't merge this change either and leave the method as in the 5.4 state, if people want a different behaviour they can add a macro. |
@JeppeKnockaert This was a good solution, although it looks like So your example above will now respond correctly to the chained sort, but that can't be relied upon—add a couple more items, for example, and it breaks:
will sort (incorrectly) to:
|
@damiani |
Chaining multiple $a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
asort($a);
print_r($a);
Array
(
[0] => 1
[1] => 1
[2] => 1
[3] => 1
[4] => 1
[5] => 1
[6] => 1
[7] => 1
[8] => 1
[9] => 1
[10] => 1
[11] => 1
[12] => 1
[13] => 1
[14] => 1
[15] => 1
) ... but add a 17th item, and PHP does not maintain the original order: $a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
asort($a);
print_r($a);
Array
(
[0] => 1
[9] => 1
[15] => 1
[14] => 1
[13] => 1
[12] => 1
[11] => 1
[10] => 1
[8] => 1
[1] => 1
[7] => 1
[6] => 1
[5] => 1
[4] => 1
[3] => 1
[2] => 1
[16] => 1
) That means the results of your first For example, given this collection: $names = collect([
['first' => 'Ann', 'last' => 'Smith'],
['first' => 'Alex', 'last' => 'Doe'],
['first' => 'Boris', 'last' => 'Doe'],
['first' => 'Betty', 'last' => 'Smith'],
['first' => 'Arthur', 'last' => 'Doe'],
['first' => 'Ann', 'last' => 'Brown'],
['first' => 'Zephyr', 'last' => 'Brown'],
]); If you try 6 => array:2 [
"first" => "Zephyr"
"last" => "Brown"
]
5 => array:2 [
"first" => "Ann"
"last" => "Brown"
]
1 => array:2 [
"first" => "Alex"
"last" => "Doe"
]
4 => array:2 [
"first" => "Arthur"
"last" => "Doe"
]
2 => array:2 [
"first" => "Boris"
"last" => "Doe"
]
0 => array:2 [
"first" => "Ann"
"last" => "Smith"
]
3 => array:2 [
"first" => "Betty"
"last" => "Smith"
] To get around this problem, I wrote a collection macro called \Illuminate\Support\Collection::macro(
'sortByMultiple',
function (array $keys) {
return collect($keys)
->reverse()
->reduce(function ($carry, $item) use (&$count) {
$key = ltrim($item, '+-');
$descending = $item[0] === '-';
return $count++
? $carry->groupBy($key)
->sortKeys(SORT_REGULAR, $descending)
->flatten(1)
: $carry->sortBy($key, SORT_REGULAR, $descending);
}, $this);
}
);
0 => array:2 [
"first" => "Ann"
"last" => "Brown"
]
1 => array:2 [
"first" => "Zephyr"
"last" => "Brown"
]
2 => array:2 [
"first" => "Alex"
"last" => "Doe"
]
3 => array:2 [
"first" => "Arthur"
"last" => "Doe"
]
4 => array:2 [
"first" => "Boris"
"last" => "Doe"
]
5 => array:2 [
"first" => "Ann"
"last" => "Smith"
]
6 => array:2 [
"first" => "Betty"
"last" => "Smith"
] This macro will sort in descending order if the key is prefixed with
Note that this macro relies on the collection method \Illuminate\Support\Collection::macro(
'sortKeys',
function ($options = SORT_REGULAR, $descending = false) {
$items = $this->items;
$descending
? krsort($items, $options)
: ksort($items, $options);
return new static($items);
}
); |
This is a repost of #21364, but this time as a request for 5.6, as the sort shouldn't change anymore in 5.5 (see the previously mentioned pull request).
This pull request is related to #21270 and #21214).
Problem
Suppose we have a collection like this:
We want everything sorted by grade first; if the grades are equal, we want to sort by name. In Laravel 5.4, we could do this:
Because of how sortBy was changed to work stable with duplicate entries in 5.5 (cfr. pull requests mentioned earlier), this doesn't work anymore.
The result from the chained sortBy() methods is this in Laravel 5.5:
This is not the expected behaviour: Cathy should come before Dave.
Cause
The cause of this becomes clear when we look at the keys of the array between the two sort operations. After performing the
sortBy('name')
, this is the collection:When we try to sort by grade now, it will see the grade of Cathy and Dave as equal and sort based on theirs keys instead (this was introduced by the stable sort).
Solution
One possible solution to this is to add a
->values()
call in the chain:This will reset the keys, resulting in key '2' for Cathy and key '3' for Dave.
I've used this idea to try and implement a fix for this issue. Instead of using
$this->items
directly, I use the values of$this->items
. The keys for these values are used in thearray_multisort
call to make sure we don't lose the stable sort.However, the keys of the values array, are not the original keys (which we want to keep too, especially when working with associative arrays). This is why I keep the
$resultKeys
variable containing the mapping from the keys of the values array to the original keys.