2

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']);
    });
}
2
  • When you say you want them in $order values, I assume you mean 'Senior Manager' and then 'Manager' etc., what if there are ones with 'Senior Manager' and 'Manager' and another with 'Senior Manager' and 'Supervisor'? Commented Oct 31, 2019 at 19:01
  • @NigelRen I think as long as the Senior Managers come first it'll be fine. Commented Nov 1, 2019 at 10:09

2 Answers 2

2

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"
    }
  }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks! Yea looking at my own code I understand why the array needs to be reversed. I'll blame it on lack of sleep :P 1/-1 was just my bad :P But will the users not in one of the titles still be sorted by name in this case? Or should I do that before running this sort? Any way to do it all in one usort call?
@powerbuoy Users without titles will be at the end of the array as you can see. Are those datas retrieved from a database? You could do the sorting in the database level instead if that's the case.
From an API I have no control over, but I'll just do a aname sort before this one I guess?
Actually no, I couldn't get that to work. If I only do a name sort it works fine, if I only do the titles sort that works fine (with your updates), but if I do a name sort before the titles sort the name sort has no effect. Could it all be done inside one usort() instead?
1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.