PHP sort positions with null values but maintain structure

Viewed 62

I'd like to sort an array with values that may or not exist in the database and the order structure should be respected.

Structure by default (positions from 1 to 5):

Amazon | Google | Ebay | Microsoft | Alibaba

This structure is initialized in PHP this way:

$data = 
[
    'Amazon'    => ['position' => null],
    'Google'    => ['position' => null],
    'Ebay'      => ['position' => null],
    'Microsoft' => ['position' => null],
    'Alibaba'   => ['position' => null]
];

Important: The positions stored in the database are always equal or bigger than 1.

Lets say Google has the position 1 and Alibaba 4 in the database:

$data['Google']['position'] = $fromDb->google->position; // 1
$data['Alibaba']['position'] = $fromDb->alibaba->position; // 4

If I sort the array using the array_multisort function as follows:

$sort = [];

foreach ($data as $key => $value)
    $sort[$key] = $value['position'];

array_multisort($sort, SORT_ASC, $data);

Output:

Array
(
    [Amazon] => 
    [Ebay] => 
    [Microsoft] => 
    [Google] => 1
    [Alibaba] => 4
)

Desired output:

Array
(
    [Google] => 1
    [Amazon] => 2
    [Ebay] => 3
    [Alibaba] => 4
    [Microsoft] => 5
)
4 Answers

Try :

$sort = [];
$null_position = 1;
arsort($data);
foreach ($data as $key => $value){
    while(in_array($null_position,array_column($data,'position'))) {
       $null_position++;
    }
    $sort[$key] = $value['position'] ?? $null_position++;
}
asort($sort);

instead of

$sort = [];

foreach ($data as $key => $value)
    $sort[$key] = $value['position'];

array_multisort($sort, SORT_ASC, $data);

Filling in the missing values before doing a simple usort:

$data = [
    'Amazon'    => ['position' => null],
    'Google'    => ['position' => 1],
    'Ebay'      => ['position' => null],
    'Microsoft' => ['position' => null],
    'Alibaba'   => ['position' => 4]
];

// Find existing positions.
$positions = array_filter(array_column($data, 'position'));
$i = 1;

foreach ($data as &$comp) {
    if ($comp['position']) {
        // Element already has a position, skip to next one.
        continue;
    }
    while (in_array($i, $positions)) {
        // Increment the counter until we find a value not yet taken.
        $i++;
    }
    // Assign the counter value to the current element.
    $comp['position'] = $i++;
}
unset($comp);

// Sort all values with a simple comparison function.
uasort($data, function ($a, $b) { return $a['position'] <=> $b['position']; });

Somewhat fancier:

// Compute the *missing* positions by subtracting the existing positions
// (extracted via array_column) from the set of possible positions
// (generated with range()).
$positions = array_diff(range(1, count($data)), array_filter(array_column($data, 'position')));

// Apply the missing positions to the array elements in order
// (taking positions off the beginning with array_shift).
array_walk($data, function (&$i) use (&$positions) {
    if (!$i['position']) {
        $i['position'] = array_shift($positions);
    }
});

// Sort all values with a simple comparison function.
uasort($data, function ($a, $b) { return $a['position'] <=> $b['position']; });

Solved.

Not sure if this is the best approach, but the output is the desired.

// Create three arrays to save the positions being used, etc
$itemsWithoutPosition = [];
$itemsWithPosition = [];
$listOfPositionsUsed = [];

// First grab the positions used
foreach ($original as $key => $value)
{
    if ($value['position'])
    {
        $itemsWithPosition[$key] = ['position' => $value['position']];
        array_push($listOfPositionsUsed, $value['position']);
    }
}

// Function to find the next available position
function findAvailablePosition($listOfPositionsUsed)
{
    for ($i = 1; $i <= 5; $i++)
    {
        if (in_array($i, $listOfPositionsUsed))
            continue;

        return $i;
    }
}

// Loop through the items that do not have position set
foreach ($original as $key => $value)
{
    if (!$value['position'])
    {
        $position = findAvailablePosition($listOfPositionsUsed);
        $itemsWithoutPosition[$key] = ['position' => $position];

        array_push($listOfPositionsUsed, $position);
    }
}

// Merge both results
$result = array_merge($itemsWithPosition, $itemsWithoutPosition);

// Sort the results 
array_multisort($result, SORT_ASC);

// Voilá
Array
(
    [Google] => Array
        (
            [position] => 1
        )

    [Amazon] => Array
        (
            [position] => 2
        )

    [Ebay] => Array
        (
            [position] => 3
        )

    [Alibaba] => Array
        (
            [position] => 4
        )

    [Microsoft] => Array
        (
            [position] => 5
        )

)

I was able to achieve it with two for loops and a temp table. The $data_full is the table that we'll be working on. And the $data is the table that'll be containing only the data coming from the database.

https://wtools.io/php-sandbox/b0hU

$data_full = 
[
    'Amazon'    => ['position' => null],
    'Google'    => ['position' => null],
    'Ebay'      => ['position' => null],
    'Microsoft' => ['position' => null],
    'Alibaba'   => ['position' => null]
];

$data = 
[
    'Amazon'    => ['position' => null],
    'Google'    => ['position' => null],
    'Ebay'      => ['position' => null],
    'Microsoft' => ['position' => null],
    'Alibaba'   => ['position' => null]
];

$data['Google']['position'] = 1;
$data['Alibaba']['position'] = 4;

$sort = [];

$num = 1;
foreach ($data_full as $key => $value) { // Loop through the table we're going to insert in
    foreach($data as $key2 => $value2) { // Loop through the table containing results from the DB
        if($data[$key2]['position']==$num) { // If it exists, we insert it, and increment the number by 1, and then continue the first loop
            $data_full[$key]['position'] = $data[$key2]['position'];
            $num++;
            continue 2;
        }
    }
    $data_full[$key]['position'] = $num;
    $num++;
}
var_dump($data_full);
Related