skip to Main Content

I’ve been cracking my brain trying to solve this challenge.

PHP default sort function doesn’t provide the solution but still, using usort isn’t easy either.

So this is what I’m trying to solve. I created an array in this order:

$data = array( '_', '@', ...range(-10, 10), ...range('A', 'Z'), ...range('a', 'z') )

Now I want to sort this array using usort so that:

  • negative numbers come first,
  • uppercase letters comes next
  • _ & @ characters follows
  • lowercase letters follows
  • Then finally positive numbers ends the order

Somewhat like:

/*
array(

  "-10",
  "-9",...

  "A",
  "B",...

  "_",
  "@", // @ may come first

  "a",
  "b",...

  "1",
  "2"...

) */

Is there any method available to solve this?

What I tried?

usort($data, function($a,$b) {
    if( is_numeric($a) && (int)$a < 0 ) return -1; // take negative number to start
    else {
        if( !is_numeric($a) ) {
            if( is_numeric($b) && (int)$b > 0 ) return -1;
            else return $b < $a ? 1 : 0;
        } else return 1; // take positive number to end
    }
});

2

Answers


  1. Think about is as a hierarchy. You have 5 non-overlapping "classes" that you want to sort: negative, uppercase, symbols, lowercase, positive. So first determine the class sort, and in the case that the class is the same for both items, compare their value instead.

    class MySorter {
        const CLASS_NUM_NEG = 0;
        const CLASS_STR_UC  = 1;
        const CLASS_STR_OT  = 2;
        const CLASS_STR_LC  = 3;
        const CLASS_NUM_POS = 4;
        
        static function get_class($item) {
            switch(gettype($item)) {
                case 'integer':
                case 'float':
                    return ($item < 0) ? self::CLASS_NUM_NEG : self::CLASS_NUM_POS;
                case 'string':
                    $ord = ord($item[0]);
                    // note: below ord() calls are illustrative, and 
                    // should be replaced with non-computed values to
                    // avoid repetitive work.
                    if( $ord >= ord('A') && $ord <= ord('Z')) {
                        return self::CLASS_STR_UC;
                    } else if( $ord >= ord('a') && $ord <= ord('z')) {
                        return self::CLASS_STR_LC;
                    } else {
                        return self::CLASS_STR_OT;
                    }
                default:
                    throw new Exception("Unhandled type: " . gettype($item));
            }
        }
        
        static function compare($a, $b) {
            $res = self::get_class($a) <=> self::get_class($b);
            if( $res !== 0 ) { return $res; }
            return $a <=> $b;
        }
    }
    
    $data = [ '_', '@', ...range(-10, 10), ...range('A', 'Z'), ...range('a', 'z') ];
    
    usort($data, ['MySorter', 'compare']);
    
    echo json_encode($data);
    

    Aside: Classes can be useful as ersatz namespaces to contain related functions and vars, so that you can box out the logic a bit better than something completely inline or dumping things in the local/global namespace.

    Output:

    [-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","@","_","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z",0,1,2,3,4,5,6,7,8,9,10]
    
    Login or Signup to reply.
  2. Perhaps using regex will be harder for some developers to read/maintain and I didn’t bother comparing the performance, but it does afford some nice code brevity. Order a series of optional capture groups in the regex pattern. The regex engine will try to satisfy the earliest occurring subpattern and the remaining capture groups will not even be represented in the matches array ($m[]). Then, because PHP sorts arrays by their count before comparing the actual data, the entries in $m which have the fewest elements will be ordered first by array_multisort(). This can, of course, be enhanced to respect multibyte strings using the u pattern modifier.

    Code: (Demo)

    $m = [];
    foreach ($data as $v) {
        preg_match('/(-d+)?([A-Z]+)?([^A-Za-z0-9]+)?([a-z]+)?(d+)?/', $v, $m[]);
        //                                                     ^^^- positive integers
        //                                            ^^^^^^- lowercase letters
        //                            ^^^^^^^^^^^^^- non-letters, non-numbers
        //                   ^^^^^^- uppercase letters
        //            ^^^^- negative integers
    }
    array_multisort($m, $data);
    var_export($data);
    

    More intuitive and easy to extend/maintain will be to use fallback comparisons with shorthand ternaries and spaceship operator comparisons until regular sorting is suitable.

    Code: (Demo) (or Demo)

    usort(
        $data,
        fn($a, $b) => ($b < 0 <=> $a < 0)                                           // prioritize negatives
                      ?: (ctype_upper((string) $b) <=> ctype_upper((string) $a))    // prioritize uppercase letters
                      ?: (is_int($a) <=> is_int($b))                                // deprioritize integers
                      ?: ($a <=> $b)                                                // sort normally
    );
    var_export($data);
    

    Alternatively, if performance is a concern, reduce the total number of required function calls by preparing arrays of evaluations, then call array_multisort(). (Demo)

    $negatives = [];
    $uppers = [];
    $integers = [];
    foreach ($data as $v) {
        $negatives[] = $v < 0;
        $uppers[] = ctype_upper((string) $v);
        $integers[] = is_int($v);
    }
    
    array_multisort(
        $negatives,
        SORT_DESC,
        $uppers,
        SORT_DESC,
        $integers,
        $data
    );
    var_export($data);
    

    Related:

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search