-1

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],
]
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Jack co
  • 1
  • 3
  • In the absence of clarity, we must assume that the "sticky position" for `lock` rows is not the index of the row, but the 1-indexed count from the start -- this means that 3rd position is the 2nd index position. This task is a combination of multiple pre-existing questions on Stack Overflow: [PHP splitting array into two arrays based on value](https://stackoverflow.com/q/20098268/2943403), [Sort multidimensional array by multiple columns](https://stackoverflow.com/a/54647220/2943403), and [Insert new item in array on any position in PHP](https://stackoverflow.com/q/3797239/2943403). – mickmackusa Aug 27 '22 at 04:20
  • I'd tweak the accepted answer [this way to change the sticky positions and ensure that whole rows are re-injected into the array](https://3v4l.org/XFMm5). – mickmackusa Aug 27 '22 at 04:20

2 Answers2

1

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 :)

IMSoP
  • 89,526
  • 13
  • 117
  • 169
0

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);
mickmackusa
  • 43,625
  • 12
  • 83
  • 136