7

I would like to sort strings in PHP, and the match should be done foremost on the first letters of a substring, then on the letters of the whole the string.

For example, if someone searches do, and the list contains

Adolf
Doe
Done

the result should be

Doe
Done
Adolf

Using the regular sort($array, SORT_STRING) or things like that does not work, Adolf is sorted before the others.

Does someone have an idea how to do that?

2
  • 1
    You can't do it with a simple search. I suggest you create multiple list, for each position of the occurrence you are looking for, then sort these sub-lists. Commented Aug 16, 2012 at 12:37
  • @user1603166, your question is slightly ambiguous. From @Roman's example, if the list also included Odometer and Abdomen, how should it be sorted? Commented Aug 16, 2012 at 13:39

4 Answers 4

3

usort(array, callback) lets you sort based on a callback.

example (something like this, didn't try it)

usort($list, function($a, $b) {
   $posa = strpos(tolower($a), 'do');
   $posb = strpos(tolower($b), 'do');
   if($posa != 0 && $posb != 0)return strcmp($a, $b);
   if($posa == 0 && $posb == 0)return strcmp($a, $b);
   if($posa == 0 && $posb != 0)return -1;
   if($posa != 0 && $posb == 0)return 1;
});
Sign up to request clarification or add additional context in comments.

4 Comments

I don't understand your answer. Ok usort let me sort with a function on my own, but the problem is still that sorting functions give me Adolf before Doe in this case.
Depending on how many comparisons are made within the usort() this can get pretty heavy :)
@user Matthews answer is better elaborated. Mine was just an example, ment as a starting point.
@Jack, it's a fair point, but your answer probably has a similar number of comparisons. However, you are caching the output of stripos(), so you are saving CPU time there at the expense of increased memory. On huge lists, it would definitely be worth benchmarking the two styles. On small lists, the difference will be irrelevant.
3

I would use a custom sort:

<?php
$list = ['Adolf', 'Doe', 'Done'];

function searchFunc($needle)
{
  return function ($a, $b) use ($needle)
  { 
    $a_pos = stripos($a, $needle);
    $b_pos = stripos($b, $needle);

    # if needle is found in only one of the two strings, sort by that one
    if ($a_pos === false && $b_pos !== false) return 1;
    if ($a_pos !== false && $b_pos === false) return -1;

    # if the positions differ, sort by the first one
    $diff = $a_pos - $b_pos;
    # alternatively: $diff = ($b_pos === 0) - ($a_pos === 0) 
    if ($diff) return $diff;

    # else sort by natural case
    return strcasecmp($a, $b);

  };
}

usort($list, searchFunc('do'));

var_dump($list);

Output:

array(3) {
  [0] =>
  string(3) "Doe"
  [1] =>
  string(4) "Done"
  [2] =>
  string(5) "Adolf"
}

4 Comments

+1. Though OP should be aware that here Odometer will be listed before Abdomen, which may or may not be desirable.
@Roman, I think that's the point of the search. But if not, removing the $diff check and return would remove that behavior.
Don't know, I presume it's used by a sort of "autocomplete" feature, in that case I'd prefer to have all results that don't begin with $needle sorted by alphabet.
@Roman, his example was ambiguous. I agree with your preference though. I think simply changing the code to be $diff = ($b_pos === 0) - ($a_pos === 0); would be sufficient if he's expecting the behavior you describe.
0

You could order the strings based on stripos($str, $search) so that the ones in the front (stripos() == 0) will come up first.

The following code pushes the substring positions of the search string into a separate array and then uses array_multisort() to apply the proper ordering to the matches; doing it this way rather than usort() avoids having to call stripos() many times.

$k = array_map(function($v) use ($search) {
    return stripos($v, $search);
}, $matches);

// $k contains all the substring positions of the search string for all matches

array_multisort($k, SORT_NUMERIC, $matches, SORT_STRING);

// $matches is now sorted against the position

4 Comments

This is a clever solution, but it will fail if the list has strings that do not contain $search. stripos() will return false, which is equated with 0. (Easily rectified if array map returns a huge number instead of false.)
@Matthew I'm assuming the matching has already been done using a grep or sth :)
Of course, ideally that should be done in the same step as the position determination ;-) let me think about that one.
You're probably right in that the search is a SELECT ... WHERE name LIKE '%$string%', in which case your answer would be sufficient. I was just pointing it out for the sake of completeness.
0

In modern PHP, writing two rules can be done more concisely than in the past.

Order by:

  1. Starts with do -- false evaluations come before true evaluations
  2. Break ties on the first rule by case-insensitively comparing the whole strings

Code: (Demo)

$array = [
    'Adolf',
    'Doe',
    'adept',
    'Done',
    'dear',
    'adopt',
    'Deer'
];

$startsWith = 'do';

usort(
    $array,
    fn($a, $b) =>
        (stripos($a, $startsWith) !== 0) <=> (stripos($b, $startsWith) !== 0)
        ?: strcasecmp($a, $b)
);
var_export($array);

More efficiently (because fewer iterated function call are made) than above, I recommend array_multisort(). The final parameter ($array) is the variable that is ultimately modified by the function.

Code: (Demo)

array_multisort(
    array_map(fn($v) => stripos($v, $startsWith) !== 0, $array),
    $array,
    SORT_STRING | SORT_FLAG_CASE,
    $array
);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.