powerbuoy
powerbuoy

Reputation: 12838

Sort multidimensional array by multiple fields

I found a few questions about this, but I feel I have a pretty special case here so I'm asking a new one.

I need to sort an array of users first by their (array) of titles and then by last name, consider the following code:

<?php
$users = [
    [
        'lastName' => 'Clarks',
        'titles' => ['Manager', 'Supervisor']
    ],
    [
        'lastName' => 'Clarkson',
        'titles' => ['Sales']
    ],
    [
        'lastName' => 'Adams',
        'titles' => ['Supervisor']
    ],
    [
        'lastName' => 'Adams',
        'titles' => ['Manager', 'Senior Manager']
    ],
    [
        'lastName' => 'Clarkson',
        'titles' => ['Manager']
    ],
    [
        'lastName' => 'Davids',
        'titles' => ['Senior Manager']
    ]
];

And the order I want is:

<?php
$order = [
    'Senior Manager',
    'Manager',
    'Supervisor'
];

If there are several managers they should be sorted by lastName, so the output in this case would be:

<?php
$sorted = [
    [
        'lastName' => 'Adams',
        'titles' => ['Manager', 'Senior Manager']
    ],
    [
        'lastName' => 'Davids',
        'titles' => ['Senior Manager']
    ],
    [
        'lastName' => 'Clarks',
        'titles' => ['Manager', 'Supervisor']
    ],
    [
        'lastName' => 'Clarkson',
        'titles' => ['Manager']
    ],
    [
        'lastName' => 'Adams',
        'titles' => ['Supervisor']
    ],
    [
        'lastName' => 'Clarkson',
        'titles' => ['Sales']
    ]
];

I've tried something along these lines but can't get it to work and find it a little hard to debug usort:

<?php
foreach ($order as $title) {
    usort($users, function ($a, $b) use ($title) {
        # Both have the title
        if (in_array($title, $a['titles']) and in_array($title, $b['titles']) ) {
            # Sort by name
            return strcmp($a['lastName'], $b['lastName']);
        }
        # A has the title
        elseif (in_array($title, $a['titles'])) {
            return 1;
        }
        # B has the title
        elseif (in_array($title, $b['titles'])) {
            return -1;
        }

        # No-one has the title
        return strcmp($a['lastName'], $b['lastName']);
    });
}

Upvotes: 2

Views: 88

Answers (2)

Fredrik Jungstedt
Fredrik Jungstedt

Reputation: 527

What you want is to sort the users based on the lowest index of their titles in $order. You could make use of array_search to find each of their titles' index in $order and find the lowest number using min. If they're the same, fall back to strcmp.

usort($users, function($a, $b) use ($order) {
    $minAPos = min(array_map(function($title) use ($order) {
        $pos = array_search($title, $order);
        return $pos === false? sizeof($order) : $pos;
    }, $a['titles']));
    $minBPos = min(array_map(function($title) use ($order) {
        $pos = array_search($title, $order);
        return $pos === false? sizeof($order) : $pos;
    }, $b['titles']));

    if($minAPos === $minBPos) {
        return strcmp($a['lastName'], $b['lastName']);
    } else {
        return $minAPos <=> $minBPos;
    }
});

Upvotes: 1

Chin Leung
Chin Leung

Reputation: 14941

The problem is that the array is sorted over and over again. So if you go step by step in the foreach loop, the items are first sorted by the Senior Manager title, then sorted again with the Manager title, and finally sorted again with the Supervisor title.

So first of all, you have to reverse the order that you want:

$order = array_reverse([
    'Senior Manager',
    'Manager',
    'Supervisor'
]);

Then you don't want to move items around if none of them have title. Because in your other iterations, your Senior Manager might not have the Supervisor title and the other user might not have the title either, therefore both of them should not change positions. So you can change the last return to return 0;

And finally, you've reversed the return of 1 and -1 so you simply need to switch them around.

Finally, the final code would look like this:

$users = [
    [
        'lastName' => 'Clarks',
        'titles' => ['Manager', 'Supervisor']
    ],
    [
        'lastName' => 'Clarkson',
        'titles' => ['Sales']
    ],
    [
        'lastName' => 'Adams',
        'titles' => ['Supervisor']
    ],
    [
        'lastName' => 'Adams',
        'titles' => ['Manager', 'Senior Manager']
    ],
    [
        'lastName' => 'Clarkson',
        'titles' => ['Manager']
    ],
    [
        'lastName' => 'Davids',
        'titles' => ['Senior Manager']
    ]
];

$order = array_reverse([
    'Senior Manager',
    'Manager',
    'Supervisor'
]);

foreach ($order as $title) {
    usort($users, function ($a, $b) use ($title) {
        if (in_array($title, $a['titles']) && in_array($title, $b['titles'])) {
            return strcmp($a['lastName'], $b['lastName']);   
        }
        # A has the title
        elseif (in_array($title, $a['titles'])) {
            return -1;
        }
        # B has the title
        elseif (in_array($title, $b['titles'])) {
            return 1;
        }

        return 0;
    });   
}

And the output would be:

array(6) {
  [0]=>
  array(2) {
    ["lastName"]=>
    string(5) "Adams"
    ["titles"]=>
    array(2) {
      [0]=>
      string(7) "Manager"
      [1]=>
      string(14) "Senior Manager"
    }
  }
  [1]=>
  array(2) {
    ["lastName"]=>
    string(6) "Davids"
    ["titles"]=>
    array(1) {
      [0]=>
      string(14) "Senior Manager"
    }
  }
  [2]=>
  array(2) {
    ["lastName"]=>
    string(6) "Clarks"
    ["titles"]=>
    array(2) {
      [0]=>
      string(7) "Manager"
      [1]=>
      string(10) "Supervisor"
    }
  }
  [3]=>
  array(2) {
    ["lastName"]=>
    string(8) "Clarkson"
    ["titles"]=>
    array(1) {
      [0]=>
      string(7) "Manager"
    }
  }
  [4]=>
  array(2) {
    ["lastName"]=>
    string(5) "Adams"
    ["titles"]=>
    array(1) {
      [0]=>
      string(10) "Supervisor"
    }
  }
  [5]=>
  array(2) {
    ["lastName"]=>
    string(8) "Clarkson"
    ["titles"]=>
    array(1) {
      [0]=>
      string(5) "Sales"
    }
  }
}

Upvotes: 2

Related Questions