phparrayssortingusortcustom-sort

Custom sort flat array by negative numbers, uppercase letters, symbols, lowercase letters, then positive numbers


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:

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
    }
});

Solution

  • 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]