Sorting in PHP is extra effort … and there is no spaceship

Monday, March 2nd, 2009 at 10:47 pm

Tonight I have been hacking in PHP on wp-lifestream in order to get grouped events ordered by time. I say hacking because there is a lot of PHP I don’t know and I don’t fully know how wp-lifestream is arranged.

When I first looked into sorting the grouped events my initial thought was to do it when the group was rendered. However that involved sorting data structures, so I decided that it would be easier to perform the sort when they were retrieved from the database. This is the change I made, and I thought it had worked.

Earlier today I found that while it worked when a feed was refreshed, it didn’t work when the group was updated after an event was deleted through the interface (I uploaded one too many photos to Flickr, which I later remved). So I had to turn back to the rendering code.

A var_dump() (Perl people should think Data::Dumper) later I knew what I was dealing with: an array of associative arrays that represented each item in the group. The relevant keys being date (in seconds since epoch) and title.

If this were perl it would be an array of hashrefs and I would have sorted it like this:

@events =
    sort { $a->{'date'} <=> $b->{'date'} || lc $a->{'title'} cmp lc $b->{'title'} } 
        @events;

Not so in PHP:

  • PHP sort functions sort the array in place instead of returning a new list
  • The more advanced PHP sort functions take a callback to use instead of a block
  • I’m not using PHP 5.3 so there are no closures yet, but at least anonymous functions can be created with create_function()
  • PHP does not have a spaceship operator (<=> for numerical comparison), even though there are equivalents of cmp (strcmp, strcasecmp).

This is what I ended up with:

usort(
    $data,
    create_function('$a,$b', '
        if ( $a["date"] == $b["date"] )
        {
            return strcasecmp( $b["title"], $a["title"] );
        }
        return ( $b["date"] < $a["date"] ) ? -1 : 1;
    ')
);

To me the logic is all messed up. Because there is no <=>, it needs a == comparison first. In the usort examples this returned 0, but gave me a convenient place to call strcasecmp() for the secondary sort. But that is breaking one sort into two statements and mixing in the other sort. It works and appears to be the PHP way of doing things, but it looks wrong to me.

(And yes, create_function() takes a string. Shudder…)

Tagged with: , , , ,

6 Responses to “Sorting in PHP is extra effort … and there is no spaceship”

  1. create_function() has real performance issues and its use is strongly discouraged. You would be better off defining the function. Like so:

    function sortByDate($item1, $item2)
    {
      if ( $item1['date'] == $item2['date'] ) {
        return strcasecmp($item1['title'], $item2['title']);
      }
      return ( $item2['date'] < $item1['date'] ) ? -1 : 1;
    }

    usort($data, ’sortByDate’);

    Far more readable :)

    Dave Hall - April 30th, 2009 at 8:15 pm

  2. Garrrr! you blog broke my formatting :( Please reformat my previous comment

    Dave Hall - April 30th, 2009 at 8:16 pm

  3. Yeah, I knew about the performance issues (cf string eval in perl), but it let me get the result I wanted in code that I wasn’t terribly familiar with.

    I initially tried to use a normal function, but I had issues with having my custom sort as a function instead of a method. To get it to work I had to declare the function a long way away from where the sort was being performed. It was easier in the long run to live with any performance impact… especially as the newer version of the Lifestream plugin has a lot of changes and I don’t know if my change is still needed.

    Formatting has been fixed :)

    Stephen - April 30th, 2009 at 9:08 pm

  4. Was grappling with the exact same issue, then realized that I could just use subtraction.

    function compare($a, $b) { return $b - $a; }

    PHP’s sort functions don’t care if you -1 or 1 exactly, they just pay attention to polarity. So anything less than 0 means that the first arg is bigger.

    Taylor - May 6th, 2010 at 6:41 am

  5. Whoops got that wrong – should have been:

    function compare($a, $b) { return $a - $b; }

    And: “anything less than 0 means that the first arg is smaller”. (See http://php.net/usort)

    Taylor - May 6th, 2010 at 6:44 am

  6. it is interesting that usort only cares about polarity, I didn’t see that at the time.

    However it would only simplify one line in what I used as I was stacking up the date and title sorts. It still needs to run the == first, but then the last return didn’t need the ternary:

    return $item2['date'] < $item1['date'];

    Stephen - May 6th, 2010 at 9:27 am