Sort an array of rows by two columns while not moving "locked" (sticky) rows

142 Views Asked by At

I need to sort an array of items by score, then by date without moving the rows where the lock value is greater than zero.

In other words, the rows with lock=3 and lock=5 should stay in the same/original position after the sorting is finished.

[
    ['id' => 7867867, 'lock' => 0, 'score' => 322, 'strtotime' => 16614713],
    ['id' => 7867867, 'lock' => 0, 'score' => 444, 'strtotime' => 16614613],
    ['id' => 7867867, 'lock' => 3, 'score' =>   0, 'strtotime' => 16613713],
    ['id' => 7867867, 'lock' => 0, 'score' =>  11, 'strtotime' => 16612713],
    ['id' => 7867867, 'lock' => 5, 'score' =>   0, 'strtotime' => 16614413],
    ['id' => 7867867, 'lock' => 0, 'score' =>  42, 'strtotime' => 16614113],
    ['id' => 7867867, 'lock' => 0, 'score' =>  22, 'strtotime' => 16614013],
]

I use the following code to sort on score than strtotime, but this affects the rows that shouldn't move.

usort($array, function ($a, $b) {
    if ( $a->score == $b->score ) {  //score are same
       return $b->strtotime <=> $a->strtotime; //sort by strtotime
    }
    return $b->score <=> $a->score; //else sort by score
});

My desired output is:

[
    ['id' => 7867867, 'lock' => 0, 'score' =>  11, 'strtotime' => 16612713],
    ['id' => 7867867, 'lock' => 0, 'score' =>  22, 'strtotime' => 16614013],
    ['id' => 7867867, 'lock' => 3, 'score' =>   0, 'strtotime' => 16613713],
    ['id' => 7867867, 'lock' => 0, 'score' =>  42, 'strtotime' => 16614113],
    ['id' => 7867867, 'lock' => 5, 'score' =>   0, 'strtotime' => 16614413],
    ['id' => 7867867, 'lock' => 0, 'score' => 322, 'strtotime' => 16614713],
    ['id' => 7867867, 'lock' => 0, 'score' => 444, 'strtotime' => 16614613],
]
2

There are 2 best solutions below

0
IMSoP On BEST ANSWER

During a sort, you have no access to the absolute position in the list, only the pair of items being compared, so I would approach it this way:

  1. Take the "locked" values out of the list
  2. Sort everything else
  3. Put the locked values back in

For step 1, just loop over array, producing two new arrays:

$result = [];
$locked = [];
foreach ( $input as $item ) {
    if ( $item['lock'] > 0 ) {
        $locked[] = $item;
    }
    else {
        $result[] = $item;
    }
}

Step 2 is the code you already have, using the array of unlocked items, which I've called $result, because it will eventually contain the final result.

For step 3, you can use array_splice to put an item into the array at a chosen position, shifting everything afterwards down.

An important thing to note here is that the order you insert in matters: if you insert item X into position 5, then item Y into position 3, position X will be shifted forward to position 6. So if your locked items aren't already in order, sort them:

usort($locked, fn($a,$b) => $a['lock'] <=> $b['lock']);

Then loop over, and splice them into their desired positions:

foreach ( $locked as $item ) {
    array_splice($result, $item['lock'], 0, $item);
}

And then you should be done :)

0
mickmackusa On

Without any filtering, temporary arrays, or splicing, I've crafted a manual sorting algorithm with three nested loops (with conditional short circuiting for best performance) to provide a "sticky sort".

There are no function calls, so it will execute rather quickly. I've added inline comments to help with code comprehension.

The below snippet does not care what the non-zero lock values are. In the posted question, the locked rows are already in the location that they should be in the result. The algorithm simply never moves rows with non-zero lock values. This makes the script easily adaptable to other scenarios where another flag indicates "fixed rows".

Code: (Demo)

$maxIndex = count($array) - 1;
for ($a = 0; $a < $maxIndex; ++$a) {
    if ($array[$a]['lock'] !== 0) {
        continue;  // cannot move locked row
    }
    for ($b = 0; $b < $maxIndex; ++$b) {
        if ($array[$b]['lock'] !== 0) {
            continue;  // cannot move locked row
        }
        // find next movable row
        for ($c = $b + 1; $c <= $maxIndex; ++$c) {
            if ($array[$c]['lock'] === 0) {
                break;  // $c is index of non-locked row
            }
        }
        if ($c > $maxIndex) {
            break;  // no more movable rows
        }
        // sort movable rows
        if (
            $array[$b]['score'] > $array[$c]['score']
            || ($array[$b]['score'] === $array[$c]['score']
                && $array[$b]['strtotime'] > $array[$c]['strtotime'])
        ) {
             [$array[$b], $array[$c]] = [$array[$c], $array[$b]];
        }
    }
}
var_export($array);