2

I have a multidimensional array that I'd like to sort based on the distance value where the false values should be added to the end in their former positions regarding their arrays.

$array = array(
        array('name' => 'Array 1', 'distance' => 3.4),
        array('name' => 'Array 2', 'distance' => 2.4),
        array('name' => 'Array 3', 'distance' => false),
        array('name' => 'Array 4', 'distance' => 5.4),
        array('name' => 'Array 5', 'distance' => false),
        array('name' => 'Array 6', 'distance' => 1),
        array('name' => 'Array 7', 'distance' => false),
        array('name' => 'Array 8', 'distance' => false),
        array('name' => 'Array 9', 'distance' => 3.6),
    );

I'd like to sort it to this:

Array 6 1
Array 2 2.4
Array 1 3.4
Array 9 3.6
Array 4 5.4
Array 3 
Array 5 
Array 7 
Array 8 

I can only get it to this:

Array 6 1
Array 2 2.4
Array 1 3.4
Array 9 3.6
Array 4 5.4
Array 8 
Array 5 
Array 3 
Array 7 

My attempt:

usort($array, function($a, $b){
    if(!$b['distance'])
        return -1;
    elseif(!$a['distance'])
        return 1;

    return $a['distance'] - $b['distance'];
});

EDIT: added my solution as an answer

  • note that a distance of `0` will be treated as a boolean, use `is_bool` instead, or `$b['distance'] === true || $b['distance'] === false` instead (`===` for type and value check) – Elias Van Ootegem Jun 05 '15 at 14:05
  • If distances are equal or both are NULL, compare Name – splash58 Jun 05 '15 at 14:13
  • @EliasVanOotegem distance will never be 0, but thanks for the observation –  Jun 05 '15 at 14:51
  • @splash58 this is just a dummy array. The real one doesn't contain a value like the Name here, so I can't do that :) –  Jun 05 '15 at 14:52
  • 1
    @ifsession then array_filter to select null element array, remove them from array, sort others and merge – splash58 Jun 05 '15 at 15:05

5 Answers5

1
// Filter items with NULL in distance
$withoutDistance = array_filter($array, function($item) { return !$item["distance"];});
// Take the rest 
$withDistance = array_diff_assoc($array, $withoutDistance);
// Sort by distance
usort($withDistance, function($a, $b){ 
      if ( $a['distance'] == $b['distance']) return 0;;
      return $a['distance'] > $b['distance'] ? 1: -1; });
// Return NULL distance into array
$array = array_merge($withDistance, $withoutDistance);
splash58
  • 26,043
  • 3
  • 22
  • 34
0

PHP's sorting functions are not 'stable'. So you cannot predict in which order the result will be if the compare function returns 0. You'll have to write your own function, as indicated here.

Community
  • 1
  • 1
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
0

This seems to do exactly what you want, but keep in mind the array_reverse!.

usort($array, function($a, $b){

    $isBFalse = is_bool($b['distance']) && $b['distance'] == false;
    $isAFalse = is_bool($a['distance']) && $a['distance'] == false;

    if($isBFalse && $isAFalse){
        return 0;
    }else if($isBFalse){
        return 1;
    }
    else if($isAFalse){
        return -1;
    }

    return $a['distance'] - $b['distance'];
});

$array = array_reverse($array);
mraiur
  • 44
  • 3
  • The values with a distance are sorted DESC, but should be ASC. You can get the same result if you only use return `$a['distance'] - $b['distance'];` –  Jun 05 '15 at 14:55
0
$withDistance = array();
$withoutDistance = array_filter($array, function($array) use (&$withDistance) {
    if($array['distance'] !== false) {
        $withDistance[] = $array;
        return false;
    }
    return true;
});

usort($withDistance, function($a, $b){
    if ($a['distance'] == $b['distance']) return 1;
    return $a['distance'] > $b['distance'] ? 1: -1;
});

$array = array_merge($withDistance, $withoutDistance);
  • you did understand my comment right. I only slightly adjusted your code – splash58 Jun 05 '15 at 18:20
  • This one is faster. `if ( $a['distance'] == $b['distance']) return 0;` didn't sort the array correctly (if two had the same distance, they weren't sorted based on their former position) `return 1` fixes the problem. –  Jun 05 '15 at 22:49
  • I did not think about this – splash58 Jun 06 '15 at 06:40
0

A solution that sorts the array in two steps. The first step computes for each element a sort key and stores it in the element itself. The second step does the actual sort using a simple string comparison of the sort keys created on the first step.

The code:

$new = array_map(
    function (array $item) {
        return array(
            'name'     => $item['name'],
            'distance' => $item['distance'],
            'sort_key' => sprintf('%020.6f/%s',
                    $item['distance'] === false ? 999999 : $item['distance'],
                    $item['name']),
        );
    },
    $array
);

usort(
    $new,
    function (array $a, array $b) {
        return strcmp($a['sort_key'], $b['sort_key']);
    }
);

The sorted array is in variable $new, the original array $array is not affected.

How it works

The first step generates a new array. Each entry in the array contains the original entry from $array plus an extra property named sort_key.

The value of sort_key is a string that is used for sorting. The sorting criteria are:

  • compare the value of distance and put false at the end; that means false is bigger than any non-false value;
  • on tie, use the value of name;

In order to sort false as greater than any non-false distance, replace false with a big number. The code above uses 999999. Adjust this value to be sure it is bigger than any distance.

Because the strings are sorted using the dictionary order, the value of distance (with false replaced with 999999) is formatted as a fixed-length string (20 characters, 6 of them are decimals), left-padded with 0.

The name is appended to this string; it is already string and it does not need any other processing.

sprintf('%020.6f/%s',
           $item['distance'] === false ? 999999 : $item['distance'],
           $item['name']),
);

For Array 1, the sprintf() above generates:

0000000000003.400000/Array 1
Community
  • 1
  • 1
axiac
  • 68,258
  • 9
  • 99
  • 134