Skip to content
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.5] Maintain original order of collection items with equal values when using sortBy() #21214

Merged
merged 1 commit into from
Sep 17, 2017

Conversation

damiani
Copy link
Contributor

@damiani damiani commented Sep 17, 2017

This PR makes the collection sortBy() method perform a stable sort, such that it preserves the original order of collection items which have identical values.

The problem

sortBy() relies on asort, which, as @sisve pointed out during this discussion in laravel/internals, includes this warning:

If two members compare as equal, their relative order in the sorted array is undefined.

So, for example, if you run sortBy('letter') on this collection:

collect([
    ['letter' => 'b'],
    ['letter' => 'a'],
    ['letter' => 'a'],
]);

...the resulting collection swaps the order of the two 'letter' => 'a' items:

Illuminate\Support\Collection {#54
  #items: array:3 [
    2 => array:1 [
      "letter" => "a"
    ]
    1 => array:1 [
      "letter" => "a"
    ]
    0 => array:1 [
      "letter" => "b"
    ]
  ]
}

This behavior varies depending on the nature of the collection—sometimes the original order will be maintained, sometimes it won't.

Why it matters

This becomes a particular problem when trying to sort a collection by multiple criteria. One way to do so is to chain sortBy() clauses in reverse order, i.e. in order to sort by "foo then bar then baz", you should be able to call:

$collection->sortBy('baz')->sortBy('bar')->sortBy('foo')

...but, because the original position of duplicate values isn't necessarily maintained between each call to sortBy(), this will sometimes result in a correct multi-sort, but not always. The results are unpredictable.

The solution

The simplest solution I've found involves using the amusingly-named Schwartzian Transform, which basically involves converting each value in the array you're sorting (['b', 'a', 'a']) into an array that includes its index ([['b', 0], ['a', 1], ['a', 2]])—which asort will handle correctly—and then stripping out those added keys once the sorting is done. Luckily, the way the sortBy() method is written, this is easy—we only need to add the keys during the first loop.

All in all, an additional 8 characters of code...and a much, much longer description. 😄

@themsaid
Copy link
Member

Reverted in #21255

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants