php - Sort array of strings which may containing day and abbreviated month names

926

I want to sort an array in an ascending direction, but a normal sort approach will not respect the optionally occurring day and month substrings. The strings containing "Feedback" must be sorted by month then day as they appear on the calendar.

Not evennatsort() will respect the months as I require.

Example array:

$array = [
    "Feedback 13 okt",
    "Feedback 11 okt",
    "Feedback 12 okt",
    "Sweet",
    "Feedback 9 okt",
    "Feedback 6 okt",
    "Feedback 8 jun",
    "Fixes",
    "Realisation",
    "Feedback 22 mar",
    "Do something",
    "Feedback 3 maj",
    "Feedback 1 dec",
];

Desired result:

[
    'Do something',
    'Feedback 22 mar',
    'Feedback 3 maj',
    'Feedback 8 jun',
    'Feedback 6 okt',
    'Feedback 9 okt',
    'Feedback 11 okt',
    'Feedback 12 okt',
    'Feedback 13 okt',
    'Feedback 1 dec',
    'Fixes',
    'Realisation',
    'Sweet',
]
248

Answer

Solution:

You should use usort() to implement your own comparison function which first compares two strings without any numbers (use preg_replace('/\d+/', '', $str) for this), and then, if the two strings compared as equal, use strnatcmp() to compare the strings (including numbers) in a natsort() way.

usort($array, function($a, $b) {
    $cmp = strcmp(preg_replace('/\d+/', '', $a), preg_replace('/\d+/', '', $b));
    if ($cmp) {
        return $cmp;
    } else {
        return strnatcmp($a, $b);
    }
});
333

Answer

Solution:

Oddly, it seems my interpretation of the required sorting logic is more in-depth than the asker's.

I see the rules as:

  1. Sort by the first word, then if a tie,
  2. If a "Feedback..." string, then parse the string and sort by month, then by day, then if still a tie,
  3. Sort on the full string "naturally" and case-insensitively.

Code: (Demo)

$array = [
    "Feedback 13 okt",
    "Feedback 11 okt",
    "Feedback 12 okt",
    "Sweet",
    "Feedback 9 okt",
    "Feedback 6 okt",
    "Feedback 8 jun",
    "Fixes",
    "Realisation",
    "Feedback 22 mar",
    "Do something",
    "Feedback 3 maj",
    "Feedback 1 dec",
];

$mmm = array_flip([
    "jan", "feb", "mar", "apr",
    "maj", "jun", "jul", "aug",
    "sep", "okt", "nov", "dec"
]);

usort(
    $array,
    fn($a, $b) =>
        strtok($a, ' ') <=> strtok($b, ' ')

        ?: (sscanf($a, "Feedback %d %s", $aDay, $aMon)
            ? [$mmm[$aMon] ?? 13, $aDay]
            : [13, 32])
           <=>
           (sscanf($b, "Feedback %d %s", $bDay, $bMon)
            ? [$mmm[$bMon] ?? 13, $bDay]
            : [13, 32])

        ?: natcasecmp($a, $b)
);
var_export($array);

Output:

array (
  0 => 'Do something',
  1 => 'Feedback 22 mar',
  2 => 'Feedback 3 maj',
  3 => 'Feedback 8 jun',
  4 => 'Feedback 6 okt',
  5 => 'Feedback 9 okt',
  6 => 'Feedback 11 okt',
  7 => 'Feedback 12 okt',
  8 => 'Feedback 13 okt',
  9 => 'Feedback 1 dec',
  10 => 'Fixes',
  11 => 'Realisation',
  12 => 'Sweet',
)

By using the "Elvis operator" (?:), subsequent tiebreaker processes are only executed if necessary. This is best practice and offers best performance.


Alternate syntax for the above approach leveraging the$mmm lookup array: (Demo)

usort(
    $array,
    function($a, $b) use($mmm) {
        sscanf("$a 32 dec", "%[^0-9] %d %s", $aWord, $aDay, $aMon);
        sscanf("$b 32 dec", "%[^0-9] %d %s", $bWord, $bDay, $bMon);
        return [$aWord, $mmm[$aMon], $aDay] <=> [$bWord, $mmm[$bMon], $bDay];
    }
);

If comfortable with regex, make a single pass over the array to make a modified version of the strings which can be sorted naturally (again leveraging the$mmm lookup array).

Code: (Demo)

array_multisort(
    preg_replace_callback(
        '/(.+) (\d+?) ([a-z]{3}?)/',
        fn($m) => "{$m[1]} {$mmm[$m[3]]} {$m[2]}",
        $array
    ),
    SORT_NATURAL,
    $array
);
421

Answer

Solution:

You can create aDateTime from the date part and sort the dated entries chronologically inusort. I was not able to get my locale settings right to work with your language so I had to edit the data to use English month abbreviations. I assume you have your PHP properly configured for your locale so this should work on your system with your data.

usort($array, function($a, $b) { 
  $pattern = '/^\w*\s+/'; // ("Feedback ") matches first word + whitespaces
  $fmt = 'j M';
  $a_date = DateTime::createFromFormat($fmt, preg_replace($pattern, '', $a));
  $b_date = DateTime::createFromFormat($fmt, preg_replace($pattern, '', $b));
  if($a_date && $b_date) // both are dates
    return $a_date <=> $b_date;
  // fallback to compare as strings
  return strcmp($a, $b);
});

So with the following$array value:

$array = [
    "Feedback 13 oct",
    "Feedback 11 oct",
    "Feedback 12 oct",
    "Sweet",
    "Feedback 9 oct",
    "Feedback 6 oct",
    "Feedback 8 jun",
    "Fixes",
    "Realisation",
    "Feedback 22 mar",
    "Do something",
    "Feedback 3 may",
    "Feedback 1 dec",
];

After calling theusort function abovevar_export($array); will output the following:

array (
  0 => 'Do something',
  1 => 'Feedback 22 mar',
  2 => 'Feedback 3 may',
  3 => 'Feedback 8 jun',
  4 => 'Feedback 6 oct',
  5 => 'Feedback 9 oct',
  6 => 'Feedback 11 oct',
  7 => 'Feedback 12 oct',
  8 => 'Feedback 13 oct',
  9 => 'Feedback 1 dec',
  10 => 'Fixes',
  11 => 'Realisation',
  12 => 'Sweet',
)

See it working here: https://onlinephp.io/c/dc5d6

People are also looking for solutions to the problem: php - Generate checkbox based on entries on a MySQL table

Source

Didn't find the answer?

Our community is visited by hundreds of web development professionals every day. Ask your question and get a quick answer for free.

Ask a Question

Write quick answer

Do you know the answer to this question? Write a quick response to it. With your help, we will make our community stronger.

Similar questions

Find the answer in similar questions on our website.