Search code examples
phparrayssortingusortstable-sort

Why does usort() not provide a stable sort for equally compared values under PHP8?


I'm using usort to sort an array with an associative array within each element.

When all of the values I am sorting on in the array are the same then it still changes the position of the elements in the array, is there a way to prevent this?

For example this:

array(
    array('name' => 'Ben', 'authn_weight' => 85.3),
    array('name' => 'Josh', 'authn_weight' => 85.3),
    array('name' => 'Fred', 'authn_weight' => 85.3)
);

May be changed to this:

array(
    array('name' => 'Josh', 'authn_weight' => 85.3),
    array('name' => 'Ben', 'authn_weight' => 85.3),
    array('name' => 'Fred', 'authn_weight' => 85.3)
);

This is the sort function:

private function weightSortImplementation($a, $b){ 
    $aWeight = $a['autn_weight'];
    $bWeight = $b['autn_weight'];

    if ($aWeight == $bWeight) {
        return 0;
    }
    return ($aWeight < $bWeight) ? 1 : -1;
}

I have checked that the weightSortImplementation function is always returning 0 showing that they are the same. So why is this still reordering the array?


Solution

  • Aha, a case for the Schwartzian Transform.

    It basically consists of three steps:

    1. decorate; you turn every value into an array with the value as the first element and the key/index as the second
    2. sort (as per normal)
    3. undecorate; you reverse step 1

    Here it is (I've tweaked it to your particular use case):

    function decorate(&$v, $k)
    {
        $v['authn_weight'] = array($v['authn_weight'], $k);
    }
    
    function undecorate(&$v, $k)
    {
        $v['authn_weight'] = $v['authn_weight'][0];
    }
    
    array_walk($a, 'decorate');
    usort($a, 'weightSortImplementation');
    array_walk($a, 'undecorate');
    

    The trick is in the following assertion:

    array($x, 0) < array($x, 1)
    

    This is what keeps the correct order of your array. And, no recursion required :)