Sort delimited array values by first half ascending then by second half descending

47 views Asked by At

I am trying to sort my array values with sizes but the result is not as I expected. This is my code:

function cmp($a, $b) {
    $sizes = array(
        "XS" => 0,  
        "S" => 1,
        "M" => 2,
        "L" => 3,
    );

    $size_a = explode("_", $a)[1];
    $size_b = explode("_", $b)[1];

    return $sizes[$size_a] <=> $sizes[$size_b];
}
    
$array = array("GL001_M","GL001_XS","GL001_S",                  
                "GL001_L","GL002_M","GL002_XS",
                "GL002_S","GL002_L");

usort($array,"cmp");

foreach($array as $arrayItem){
    echo $arrayItem.' | ';
}

My output is this:

GL001_XS | GL002_XS | GL001_S | GL002_S | GL001_M | GL002_M | GL001_L | GL002_L | 

Instead I would like this:

GL001_XS | GL001_S | GL001_M | GL001_L | GL002_XS | GL002_S | GL002_M | GL002_L | 
2

There are 2 answers

0
Nigel Ren On BEST ANSWER

The issue with the current method is that it only sorts on the final part of each string (so the 'S', 'XS' etc.)

What you can do is expand the comparison to sort on the first part ('GL001') and only when there is a match to use the size to sort them.

$size_a = explode('_', $a);
$size_b = explode('_', $b);

return ($size_a[0] <=> $size_b[0]) ?: $sizes[$size_a[1]] <=> $sizes[$size_b[1]];

So $size_a[0] <=> $size_b[0] will compare the 'GL001' bits.

The using ?: it will return the second part if the value of the first part is 0 (i.e. they are the same).
$sizes[$size_a[1]] will be the size translated by the array (so 'S' => 1).

0
mickmackusa On

Wow, this question is super similar to Custom sort on value suffix representing sizes (XXS, XS, S, M, L, XL, XXL), but the fact that you are sorting by both halves of the input strings AND the desired size values can be sorted alphabetically, my advice will change. No lookup array is necessary. Calling explode() with linear time complexity will mean fewer explode() calls than 2 per usort() iteration.

Code: (Demo)

$ids = [];
$sizes = [];
foreach ($array as $v) {
    [$ids[], $sizes[]] = explode('_', $v, 2);
}
array_multisort($ids, $sizes, SORT_DESC, $array);
var_export($array);

Once you have to accommodate sizes greater than L, then a lookup array is needed.

Code: (Demo)

define('SIZES', array_flip(['XXS', 'XS', 'S', 'M', 'L', 'XL', 'XXL']));

$ids = [];
$sizes = [];
foreach ($array as $v) {
    [$ids[], $size] = explode('_', $v, 2);
    $sizes[] = SIZES[$size];
}
array_multisort($ids, $sizes, $array);
var_export($array);

And if you are super disgruntled with the other developers that you work with, you give them sore eyes with this fully functional-style snippet with no new, globally scoped variables! Demo

define('SIZES', array_flip(['XXS', 'XS', 'S', 'M', 'L', 'XL', 'XXL']));

array_multisort(
    ...array_merge(
        array_map(
            null,
            ...array_map(
                fn($v) =>
                    (fn($id, $size) => [$id, SIZES[$size]])(...explode('_', $v, 2)),
                    $array
            )
        ),
        [&$array]
    )
);
var_export($array);