0

I have a problem sorting some arrays because some comparing values are the same and usort can't be controlled in which order those should be returned. This is a limitation of usort which I can't overcome unless I use some custom sorting function. This is where I need the help.

I have this array:

Array
(
    [0] => Array
        (
            [date] => 23/01/2021
            [time] => 15:25
            [name] => name1
        )

    [1] => Array
        (
            [date] => 23/01/2021
            [time] => 15:27
            [name] => name2
        )

    [2] => Array
        (
            [date] => 26/01/2021
            [time] => 07:19
            [name] => name3
        )

    [3] => Array
        (
            [date] => 26/01/2021
            [time] => 07:24
            [name] => name4
        )

    [4] => Array
        (
            [date] => 27/01/2021
            [time] => 08:38
            [name] => name5
        )

    [5] => Array
        (
            [date] => 27/01/2021
            [time] => 08:38
            [name] => name6
        )

    [6] => Array
        (
            [date] => 27/01/2021
            [time] => 10:09
            [name] => name7
        )

    [7] => Array
        (
            [date] => 27/01/2021
            [time] => 16:40
            [name] => name8
        )
)

I want to sort the array in chronologial order (newest to oldest) but as you can see name5 and name6 have the same date and time. I want these to be returned in the order they appear (the smallest key first).

What I would like as an outcome for example in this specific array is a reverse order (since they are in oldest to newest) but name5 and name6 not to be reversed (because I want to keep the order they are in).

How can I do this? Not specifically for this table, but maybe on another table where 3 items have the same date/time.

Thanks

MerNion
  • 17
  • 1
  • 7
  • Does this answer your question? [Sort multidimensional array by multiple keys](https://stackoverflow.com/questions/3232965/sort-multidimensional-array-by-multiple-keys) – David Brossard Jan 27 '21 at 21:32
  • 1
    You really should store dates in `yyyy-mm-dd` format (or even `yyyy/mm/dd`), then you can sort them as strings instead of having to parse them into dates. – Nick Jan 27 '21 at 21:44

3 Answers3

0
foreach( $arr as $i => &$x ){ $x['k'] = $i; }

This adds a column to contain the key you're wanting to sort by. Then, just do your usort operation, that falls back to the key compare if date/time is equal. I added a simple helper function to fix your dates so strtotime can parse it.

function o_to_time( $o ){
    return strtotime( implode( "/" , array_reverse( explode( "/" , $o["date"] ) ) ) . " " . $o["time"] );
}

usort( $arr , function( $a , $b ){
    return o_to_time( $b ) <=> o_to_time( $a ) ?: $a['k'] <=> $b['k'];
} );
ohthefunk
  • 16
  • 3
0

In order to solve this we need to implement a function that compares the date and time and return 0, -1, or 1. This way usort will be able to sort the array. When the return is 0 the position is not changed.

Input:

  $arr = [
    ['date' => '23/01/2021', 'time' => '15:25', 'name' => ' name1'],
    ['date' => '23/01/2021', 'time' => '15:27', 'name' => ' name2'],
    ['date' => '26/01/2021', 'time' => '07:19', 'name' => ' name3'],
    ['date' => '26/01/2021', 'time' => '07:24', 'name' => ' name4'],
    ['date' => '27/01/2021', 'time' => '08:38', 'name' => ' name5'],
    ['date' => '27/01/2021', 'time' => '08:38', 'name' => ' name6'],
    ['date' => '27/01/2021', 'time' => '10:09', 'name' => ' name7'],
    ['date' => '27/01/2021', 'time' => '16:14', 'name' => ' name8']
  ];

In the callback function we pass to usort we will convert the date and time in the input array to a DateTime object so we can compare the dates. Here is a stackoverflow on how to compare DateTime objects

The DateTime class has a static function DateTime.createFromFormat that you can pass the format of the string you want to convert to a DateTime, in your case d/m/Y H:i (23/01/2021 15:25).

To order it from oldest to newest:

  usort($arr, function($a, $b) {
    $dateTimeA = DateTime::createFromFormat('d/m/Y H:i', "{$a['date']} {$a['time']}");
    $dateTimeB = DateTime::createFromFormat('d/m/Y H:i', "{$b['date']} {$b['time']}");

    if($dateTimeA == $dateTimeB) {
        return 0;
    }
    return ($dateTimeA > $dateTimeB) ? -1 : 1;
  });

To order it from newest to oldest just change > to < in the return statement:

  return ($dateTimeA < $dateTimeB) ? -1 : 1;
  • This doesn't work as intended - it doesn't handle the scenario where values are the same correctly? – Steven Jan 27 '21 at 23:42
  • It only _works_ if the keys are in the right order already. If you flip the order of the two items on input (but leave the keys as they are) then it doesn't sort them as it should do. – Steven Jan 28 '21 at 01:41
  • I see what you mean. The name key. That is true I did not take that into consideration but that is easy to add to that logic. – Francisco de Castro Jan 28 '21 at 02:57
0

This sounds like a perfect use case of uksort...

You need to use some sort of function to sort the date/times, I'd just use DateTime::createFromFormat and create the string from the date and time fields.

Obviously you need to check first if they're the same and if they are sort based on the keys...

Then if they aren't the same use the spaceship operator (<=>) to return -1|0|1 depending on the whether the value (DateTime object) on the left is smaller, the same, or greater than the one on the right - obviously it won't return 0 in this case because we already took care of that.

Input

$arr = [
    0 => [
            'date' => '23/01/2021',
            'time' => '15:25',
            'name' => 'name1',
        ],

    1 => [
            'date' => '23/01/2021',
            'time' => '15:27',
            'name' => 'name2',
        ],

    2 => [
            'date' => '26/01/2021',
            'time' => '07:19',
            'name' => 'name3',
        ],

    3 => [
            'date' => '26/01/2021',
            'time' => '07:24',
            'name' => 'name4',
        ],

    4 => [
            'date' => '27/01/2021',
            'time' => '08:38',
            'name' => 'name5',
        ],
    
    5 => [
            'date' => '27/01/2021',
            'time' => '08:38',
            'name' => 'name6',
        ],

    6 => [
            'date' => '27/01/2021',
            'time' => '10:09',
            'name' => 'name7',
        ],

    7 => [
            'date' => '27/01/2021',
            'time' => '16:40',
            'name' => 'name8',
        ],
];

Code

uksort($arr, function ($a, $b) use ($arr){
    $aDate = DateTime::createFromFormat("d/m/Y H:i", "{$arr[$a]["date"]} {$arr[$a]["time"]}");
    $bDate = DateTime::createFromFormat("d/m/Y H:i", "{$arr[$b]["date"]} {$arr[$b]["time"]}");

    if($aDate == $bDate){
        return $a <=> $b;
    }

    return $bDate <=> $aDate;
});

print_r($arr);

Compacted Code

uksort($arr, function ($a, $b) use ($arr){
    $aDate = DateTime::createFromFormat("d/m/Y H:i", "{$arr[$a]["date"]} {$arr[$a]["time"]}");
    $bDate = DateTime::createFromFormat("d/m/Y H:i", "{$arr[$b]["date"]} {$arr[$b]["time"]}");

    return $aDate == $bDate ? $a <=> $b : $bDate <=> $aDate;
});

Output

Array
(
    [7] => Array
        (
            [date] => 27/01/2021
            [time] => 16:40
            [name] => name8
        )

    [6] => Array
        (
            [date] => 27/01/2021
            [time] => 10:09
            [name] => name7
        )

    [4] => Array
        (
            [date] => 27/01/2021
            [time] => 08:38
            [name] => name5
        )

    [5] => Array
        (
            [date] => 27/01/2021
            [time] => 08:38
            [name] => name6
        )

    [3] => Array
        (
            [date] => 26/01/2021
            [time] => 07:24
            [name] => name4
        )

    [2] => Array
        (
            [date] => 26/01/2021
            [time] => 07:19
            [name] => name3
        )

    [1] => Array
        (
            [date] => 23/01/2021
            [time] => 15:27
            [name] => name2
        )

    [0] => Array
        (
            [date] => 23/01/2021
            [time] => 15:25
            [name] => name1
        )

)
Steven
  • 6,053
  • 2
  • 16
  • 28
  • The `$aDate == $bDate` check is not necessary. The spaceship operator already handles `==` in its "3-way comparison". – mickmackusa Jan 24 '23 at 22:36