marius2k12
marius2k12

Reputation: 1101

Sort each subset in a 3-level array by four column values

I have the following multi-dimensional array that I want to sort.

I want to sort the innermost arrays by total_points, then tiebraker1, 2 and 3.

Example:

[
    1 => [
        1 => [
            'userid' => 17,
            'total_points' => 16,
            'tiebraker1' => 1,
            'tiebraker2' => 2,
            'tiebraker3' => 1
        ],
        2 => [
            'userid' => 29,
            'total_points' => 16,
            'tiebraker1' => 1,
            'tiebraker2' => 2,
            'tiebraker3' => 9
        ]
    ],
    2 => [
        1 => [
            'userid' => 26,
            'total_points' => 26,
            'tiebraker1' => 2,
            'tiebraker2' => 2,
            'tiebraker3' => 4
        ],
        2 => [
            'userid' => 17,
            'total_points' => 26,
            'tiebraker1' => 3,
            'tiebraker2' => 2,
            'tiebraker3' => 4
       ]
    ]
]

Desired result:

[
    1 => [
        1 => [
            'userid' => 29,
            'total_points' => 16,
            'tiebraker1' => 1,
            'tiebraker2' => 2,
            'tiebraker3' => 9
        ],
        2 => [
            'userid' => 17,
            'total_points' => 16,
            'tiebraker1' => 1,
            'tiebraker2' => 2,
            'tiebraker3' => 1
        ]
    ],
    2 => [
        1 => [
            'userid' => 17,
            'total_points' => 26,
            'tiebraker1' => 3,
            'tiebraker2' => 2,
            'tiebraker3' => 4
        ],
        2 => [
            'userid' => 26,
            'total_points' => 26,
            'tiebraker1' => 2,
            'tiebraker2' => 2,
            'tiebraker3' => 4
       ]
    ]
]

I tried using array_multisort but I can't configure it correctly.

Upvotes: 1

Views: 1485

Answers (3)

mickmackusa
mickmackusa

Reputation: 48091

Iterate over each set of rows, preserve the keys starting from 1, sort by the last four columns, then reapply the cached keys. Demo

array_walk(
    $array,
    function (&$rows) {
        $keys = array_keys($rows);
        usort(
            $rows,
            fn($a, $b) => array_slice($b, 1)
                          <=>
                          array_slice($a, 1)
        );
        $rows = array_combine($keys, $rows);
    }
);
var_export($array);

If your userid column was the last column of all rows, then rsort() would suffice because the rows have the same element count and the column positions align with sorting priorities. Ultimately, if all scoring columns are tied, then the tie would be broken by the larger userid. Demo

array_walk(
    $array,
    function (&$rows) {
        $keys = array_keys($rows);
        rsort($rows);
        $rows = array_combine($keys, $rows);
    }
);
var_export($array);

Upvotes: 0

Martin Ender
Martin Ender

Reputation: 44289

To use array_multisort you would need a different structure for your data. Specifically you would need to group by "score type" (or expressed mathematically, transpose the array). E.g. like this using your first example:

array(5) {
    // $userid
    [0] => array(2) {
        [0] => 17
        [1] => 29
    }

    // $total_points
    [1] => array(2) {
        [0] => 16
        [1] => 16
    }

    // $tiebreaker1
    [2] => array(4) {
        [0] => 1
        [1] => 1
    }

    // $tiebreaker2
    [3] => array(2) {
        [0] => 2
        [1] => 2
    }

    // $tiebreaker3
    [4] => array(2) {
        [0] => 1
        [1] => 9
    }
}

Then you could use array_multisort() as follows:

array_multisort($ar[1], SORT_DESC, SORT_NUMERIC,
                $ar[2], SORT_DESC, SORT_NUMERIC,
                $ar[3], SORT_DESC, SORT_NUMERIC,
                $ar[4], SORT_DESC, SORT_NUMERIC,
                $ar[0], SORT_ASC, SORT_NUMERIC);

If you cannot change the structure of the array, you could use usort() instead and define the comparision criteria manually.

function cmp($a, $b)
{
    if ($a['total_points'] != $b['total_points']) {
        return ($a['total_points'] > $b['total_points']) ? -1 : 1;
    } elseif ($a['tiebreaker1'] != $b['tiebreaker1']) {
        return ($a['tiebreaker1'] > $b['tiebreaker1']) ? -1 : 1;   
    } elseif ($a['tiebreaker2'] != $b['tiebreaker2']) {
        return ($a['tiebraker2'] > $b['tiebreaker2']) ? -1 : 1;   
    } elseif ($a['tiebreaker3'] != $b['tiebreaker3']) {
        return ($a['tiebreaker3'] > $b['tiebreaker3']) ? -1 : 1;   
    } else {
        return 0;
    }
}

usort($array, "cmp");

Disclaimer: I do not claim that my implementation of cmp is the most elegant one. But it should do the trick. :)

Upvotes: 4

Bud Damyanov
Bud Damyanov

Reputation: 31919

From the PHP.net's documentation:

<?php
$ar = array(
       array("10", 11, 100, 100, "a"),
       array(   1,  2, "2",   3,   1)
      );
array_multisort($ar[0], SORT_ASC, SORT_STRING,
                $ar[1], SORT_NUMERIC, SORT_DESC);
var_dump($ar);
?> 

In this example, after sorting, the first array will transform to "10", 100, 100, 11, "a" (it was sorted as strings in ascending order). The second will contain 1, 3, "2", 2, 1 (sorted as numbers, in descending order).

array(2) {
  [0]=> array(5) {
    [0]=> string(2) "10"
    [1]=> int(100)
    [2]=> int(100)
    [3]=> int(11)
    [4]=> string(1) "a"
  }
  [1]=> array(5) {
    [0]=> int(1)
    [1]=> int(3)
    [2]=> string(1) "2"
    [3]=> int(2)
    [4]=> int(1)
  }
}

Upvotes: 0

Related Questions