How to sort a multidimensional array by multiple columns?
Asked Answered
T

5

2

I'm trying to do the same as MySQL query

SELECT * FROM table ORDER BY field1, field2, ...

but with php and a multidimensional array:

$Test = array(
    array("a"=>"004", "n"=>"03"),
    array("a"=>"003", "n"=>"02"),
    array("a"=>"001", "n"=>"02"),
    array("a"=>"005", "n"=>"01"),
    array("a"=>"001", "n"=>"01"),
    array("a"=>"004", "n"=>"02"),
    array("a"=>"003", "n"=>"01"),
    array("a"=>"004", "n"=>"01")
);
function msort(&$array, $keys){
    array_reverse($keys);
    foreach($keys as $key){
        uasort($array, sortByKey);
    }
    //
    function sortByKey($A, $B){

        global $key;

        $a = $A[$key];
        $b = $B[$key];
        if($a==$b) return 0;
        return ($a < $b)? -1 : 1 ;
    }
}
//
msort($Test, array("a","n"));
//
foreach($Test as $t){
    echo('<p>'.$t["a"].'-'.$t["n"].'</p>');
}

My theory is: if I sort multiple times on columns with "lesser importance" then columns of "greater importance", I'll achieve an order like the above MySQL query.

Unfortunately, php is returning:

Warning: uasort() expects parameter 2 to be a valid callback, function 'sortByKey' not found or invalid function name in /Library/WebServer/Documents/www/teste.array_sort.php on line 23" (uasort line)

It's a simple order function. What am I missing?

Twigg answered 2/4, 2014 at 13:13 Comment(6)
possible duplicate of Reference: all basic ways to sort arrays and data in PHPHelgeson
Maybe, I'll take a look.Twigg
The reference got a different approach. Also, before close this question that is not really just a duplicate, I think it should be answered why the code doesn't work.Twigg
It doesn't work because you're sorting by a, and then you're sorting again from scratch by b. You're not refining a sort, you're resorting by something else. Also, that's not how you pass functions as callback. Also, declaring functions within functions does not really work [the way you may think it does].Helgeson
Sorry, I didn't get your a - b explanation, but you given me a clue, so I could solve it above, using all contributions here and from the reference. And if you see it with some patience, you'll realize that this post is not a duplicate, my answer is different.Twigg
I'm saying if you first do usort by key a and then again usort by key n, you're not going to end up with a ORDER BY a, n, you'll just end up with the equivalent of ORDER BY n. One sort does not "refine" the previous one.Helgeson
H
7

Fundamentally we're going to use the same approach as explained here, we're just going to do it with a variable number of keys:

/**
 * Returns a comparison function to sort by $cmp
 * over multiple keys. First argument is the comparison
 * function, all following arguments are the keys to
 * sort by.
 */
function createMultiKeyCmpFunc($cmp, $key /* , keys... */) {
    $keys = func_get_args();
    array_shift($keys);

    return function (array $a, array $b) use ($cmp, $keys) {
        return array_reduce($keys, function ($result, $key) use ($cmp, $a, $b) {
            return $result ?: call_user_func($cmp, $a[$key], $b[$key]);
        });
    };
}

usort($array, createMultiKeyCmpFunc('strcmp', 'foo', 'bar', 'baz'));
// or
usort($array, createMultiKeyCmpFunc(function ($a, $b) { return $a - $b; }, 'foo', 'bar', 'baz'));

That's about equivalent to an SQL ORDER BY foo, bar, baz.

If of course each key requires a different kind of comparison logic and you cannot use a general strcmp or - for all keys, you're back to the same code as explained here.

Helgeson answered 3/4, 2014 at 6:16 Comment(2)
Well, your answer is more elegant than mine, to say at list. But as long you say is the same approach, you use a lot of different techniques to solve the problem, maybe I have a different understanding of "approach", but your answer is really a nice contribution to this forum. Thanks!Twigg
"Same approach" as in "we'll create one comparison function with which you'll do exactly one sort". As opposed to your initial approach of doing several successive sorts. This answer here just demonstrates a technique to dynamically create that comparison function, nothing more. :)Helgeson
G
5

Here's some code I wrote to do something similar:

uasort($array,function($a,$b) {
    return strcmp($a['launch'],$b['launch'])
        ?: strcmp($a['tld'],$b['tld'])
        ?: strcmp($a['sld'],$b['sld']);
});

It kind of abuses the fact that negative numbers are truthy (only zero is falsy) to first compare launch, then tld, then sld. You should be able to adapt this to your needs easily enough.

Gentoo answered 2/4, 2014 at 13:16 Comment(5)
Neat! I linked to it from #17364627 :)Helgeson
You can, of course, use different *cmp functions (strcasecmp, strncmp, strncasecmp, strnatcmp, strnatcasecmp) depending on the desired result.Gentoo
Or any comparison operation, really. return $a['foo'] - $b['foo'] ?: $a['bar'] - $b['bar']Helgeson
@Helgeson True! So long as the function returns zero for "they are equal", you're all good.Gentoo
The reference got similar solution. But using a function with unlimited keys we will get a solution for more cases (more generic solution). That's why I put the sql query, you have a database in a string and can sort wherever you need.Twigg
A
1

PHP7.4's arrow syntax eliminates much of the code bloat which was previously necessary to bring your column orders into the usort() scope.

Code: (Demo)

$orderBy = ['a', 'n'];
usort($Test, fn($a, $b) =>
    array_map(fn($v) => $a[$v], $orderBy)
    <=>
    array_map(fn($v) => $b[$v], $orderBy)
);
var_export($Test);

I reckon this is a very elegant and concise way to script the task. You generate the nominated column values from $a and $b as separate arrays and write the spaceship operator between them.

Without the arrow syntax, the snippet gets a little more chunky.

Code: (Demo)

$orderBy = ['a', 'n'];
usort($Test, function($a, $b) use ($orderBy) {
    return 
        array_map(function($v) use ($a){
            return $a[$v];
        }, $orderBy)
        <=>
        array_map(function($v) use ($b){
            return $b[$v];
        }, $orderBy);
});
var_export($Test);

The spaceship operator will walk through corresponding pairs of data ([0] vs [0], then [1] vs [1], and so on) until it reaches a non-zero evaluation or until it exhausts the comparison arrays.


With fewer total iterated function calls, you could call array_multisort() after preparing flat columns to sort by.

Code: (Demo)

$orderBy = ['a', 'n'];
$params = array_map(fn($colName) => array_column($Test, $colName), $orderBy);
$params[] = &$Test;
array_multisort(...$params);
var_export($Test);
Augusto answered 4/12, 2019 at 19:58 Comment(0)
T
0

This will do the job, thanks for contributions!

function mdsort(&$array, $keys){

    global $KeyOrder;

    $KeyOrder = $keys;
    uasort($array, cmp);    
}
function cmp(array $a, array $b) {

    global $KeyOrder;

    foreach($KeyOrder as $key){
        $res = strcmp($a[$key], $b[$key]);
        if($res!=0) break;
    }
    return $res;
}
//
mdsort($Test, array("a","n"));

This code is a little ugly, though, I believe it can be better - maybe a class to solve the issue of passing the array with the keys to the "cmp" function. But it's a start point, you can use it with any number of keys to sort.

Twigg answered 2/4, 2014 at 22:36 Comment(0)
G
0

Corrected your function

$Test = array(
    array("a"=>"004", "n"=>"03"),
    array("a"=>"003", "n"=>"02"),
    array("a"=>"001", "n"=>"02"),
    array("a"=>"005", "n"=>"01"),
    array("a"=>"001", "n"=>"01"),
    array("a"=>"004", "n"=>"02"),
    array("a"=>"003", "n"=>"01"),
    array("a"=>"004", "n"=>"01")
);
function msort(&$array, $keys){
    foreach(array_reverse($keys) as $key)
        usort($array, Fn ($A, $B) => $A[$key]<=>$B[$key]);
}
msort($Test, array("a","n"));

foreach($Test as $t){
    echo('<p>'.$t["a"].'-'.$t["n"].'</p>');
}

Result

001-01
001-02
003-01
003-02
004-01
004-02
004-03
005-01

or with sort direction

function msort(&$array, $keys){
    foreach(array_reverse($keys) as $key=>$sort)
        usort($array,Fn($a,$b) => $sort == SORT_DESC ? $b[$key]<=>$a[$key]:$a[$key]<=>$b[$key]);
}
msort($Test, array("a"=>SORT_DESC,"n"=>SORT_ASC));

Result

005-01
004-01
004-02
004-03
003-01
003-02
001-01
001-02
Gibbous answered 29/3, 2023 at 6:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.