phparraysstickyusortcustom-sort

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


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],
]

Solution

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