EmmyS
EmmyS

Reputation: 12138

Move elements of multidimensional array

I'm receiving an array from a database - I have no control over what pieces of data are sent and in what order. This is what it currently looks like:

Array
(
    [itemCode] => Array
        (
            [0] => Array
                (
                    [code] => P
                    [descShort] => Pepperoni
                )

            [1] => Array
                (
                    [code] => G
                    [descShort] => Green Peppers
                )

            [2] => Array
                (
                    [code] => n
                    [descShort] => No Sauce
                )

            [3] => Array
                (
                    [code] => x
                    [descShort] => No Cheese
                )

            [4] => Array
                (
                    [code] => 
                    [descShort] => Regular Cheese
                )

            [5] => Array
                (
                    [code] => 
                    [descShort] => Regular Sauce
                )

        )

)

In actual practice, there can be any number of elements before the No Sauce option (currently at index 3, but not always that way.) What the client wants is for the Cheese and Sauce items to always be at the end of the list and ordered this way: Regular Cheese, No Cheese, Regular Sauce, No Sauce.

Again, keeping in mind that I have no control over how the array is initially created, and that there could be any number of other elements both before and between the elements in question, how can I make this happen? Something else to worry about is that at some point, there may be other options they want to include in this re-ordering (they may add an option for Extra Cheese and Extra Sauce, for example, and want them to be in specific positions as well.)

Added var_export

array (
  'itemCode' => 
  array (
    0 => 
    array (
      'code' => 'P',
      'descShort' => 'Pepperoni',
    ),
    1 => 
    array (
      'code' => 'G',
      'descShort' => 'Green Peppers',
    ),

    2 => 
    array (
      'code' => 'n',
      'descShort' => 'No Sauce',
    ),
    3 => 
    array (
      'code' => 'x',
      'descShort' => 'No Cheese',
    ),
    4 => 
    array (
      'code' => '',
      'descShort' => 'Regular Cheese',
    ),
    5 => 
    array (
      'code' => '',
      'descShort' => 'Regular Sauce',
    ),
  ),
)

Upvotes: 2

Views: 2132

Answers (3)

hakre
hakre

Reputation: 197787

As it has been already commented, the usort function can be used for that. But this is only the start as it needs the compare function.

That is also not that hard, because we can just create one. However, it's crucial to understand how it works:

The comparison function must return an integer less than, equal to, or greater than zero if the first argument is considered to be respectively less than, equal to, or greater than the second.

Okay, that's not a blocker but turns the actual question, how to find out which one is above which one? And how to do that extensible?

There are two type of values: Either the ones that do not need any ordering or the ones that need ordering. Let's first define an array for those that need ordering. And to make it clear for what that ordering is, name the key:

$order    = 'descShort';
$ordering = ['Regular Cheese', 'No Cheese', 'Regular Sauce', 'No Sauce'];

Now inside a compare function, you can look up if for the A/B values an entry exists within the ordering. If it does not exists, then this needs no ordering. If it exists, this needs ordering.

These two cases are extended because you also can have an existing sort order with a non-existing one. So there are four cases to cover:

  1. A and B do not exists - treat them equal -- 0
  2. A and B exist - sort according to their order-value -- calculate position A - B
  3. A exists but not B - A is greater than B -- 1
  4. B exists but not A - A is less than B -- -1

Thanks to function support in PHP, we can pass along the order-key and the ordering values into the comparison function easily for a quick example. The function then only has to do what has been outlined for the four cases.

The example:

$order    = 'descShort';
$ordering = ['Regular Cheese', 'No Cheese', 'Regular Sauce', 'No Sauce'];
$compare  = function($a, $b) use ($order, $ordering) {
    $hasA = array_search($a[$order], $ordering);
    $hasB = array_search($b[$order], $ordering);

    // nothing to sort
    if ($hasA === $hasB && $hasA === FALSE) {
        return 0;
    }

    // if both are found, sort
    if ($hasA !== FALSE && $hasB !== FALSE) {
        return $hasA - $hasB;
    }

    // as one of them is in there, put it to end
    return $hasA === FALSE ? -1 : 1;
};

usort($array['itemCode'], $compare);

So now there is one caveat: usort is not stable. That means, when returning 0, items do not stay at their position. You can work around that by sorting once again the same with usort.

usort($array['itemCode'], $compare);

Then the final sort order is (Demo):

Array
(
    [itemCode] => Array
        (
            [0] => Array
                (
                    [code] => P
                    [descShort] => Pepperoni
                )

            [1] => Array
                (
                    [code] => G
                    [descShort] => Green Peppers
                )

            [2] => Array
                (
                    [code] => 
                    [descShort] => Regular Cheese
                )

            [3] => Array
                (
                    [code] => x
                    [descShort] => No Cheese
                )

            [4] => Array
                (
                    [code] => 
                    [descShort] => Regular Sauce
                )

            [5] => Array
                (
                    [code] => n
                    [descShort] => No Sauce
                )
        )
)

As there was a problem with stable sort and I'm also not that well with it (the suggested function looks a bit like an overhead to me in your case here), there is just good old foreach.

And as there are no duplicate values for those to be sorted later on, well, this leaves some nifty room:

$order    = 'descShort';
$ordering = ['Regular Cheese', 'No Cheese', 'Regular Sauce', 'No Sauce'];

$result = []; // the final result
$later  = []; // to be sorted later
foreach($array as $element)
{

    $has = array_search($element[$order], $ordering);
    if ($has !== FALSE) {
        $later[$has] = $element;
        continue;
    }

    $result[] = $element;
}

It is just straight ahead for-eaching over the array, putting all values into $result already that are not part of the ordering.

Those that are part of the ordering are put into $later with their order value as index already.

Only $later then is sorted with ksort, and then the two parts are merged:

ksort($later);
$result = array_merge($result, $later);

And done. No callback function needed. Just first filtering, and the indexing with the sort value together with ksort does the magic. Demo.

Upvotes: 2

Barmar
Barmar

Reputation: 781078

I think this is the user-defined sort that you want. The array $last_options lists all the toppings that should be sorted to the end, and their order in the array specifies the order they should appear in the result.

$last_options = array('Regular Cheese', 'No Cheese', 'Regular Sauce', 'No Sauce');

function topping_order($a, $b) {
  $a_pos = array_search($a['descShort'], $last_options);
  $b_pos = array_search($b['descShort'], $last_options);
  if ($a_pos !== false && $b_pos !== false) {
    if ($a_pos > $b_pos) {
      return 1;
    }
    if ($b_pos > $a_pos) {
      return -1;
    }
    return 0;
  }
  if ($a_pos === false) {
    return -1;
  }
  if ($b_pos === false) {
    return 1;
  }
  return 0;
}

usort($array, 'topping_order');

Upvotes: 0

Teena Thomas
Teena Thomas

Reputation: 5239

You can try using usort, which will allow a user-defined sort.

Upvotes: 3

Related Questions