tags:

views:

937

answers:

11

If I have an object as such:

class Person {
  var $age;
  function __construct($age) {
    $this->age = $age;
  }
}

and I have any array of Person's

$person1 = new Person(14);
$person2 = new Person(5);
$people = array($person1, $person2);

Is there an easy way to sort the $people array by the Person->age property?

+1  A: 

usort() or uasort() /* to maintain index association if you were using an associative array */

grantwparks
+7  A: 

For that specific scenario, you can sort it using the usort() function, where you define your own function to compare the items in the array.

<?php

class Person {
  var $age;
  function __construct($age) {
    $this->age = $age;
  }
}

function personSort( $a, $b ) {
    return $a->age == $b->age ? 0 : ( $a->age > $b->age ) ? 1 : -1;
}

$person1 = new Person(14);
$person2 = new Person(5);
$person3 = new Person(32);
$person4 = new Person(150);
$person5 = new Person(39);
$people = array($person1, $person2, $person3, $person4, $person5);

print_r( $people );

usort( $people, 'personSort' );

print_r( $people );
meder
I'm trying to avoid usort() because it is too expensive of a call as my people array grows. Let's assume I have 15,000 entries in $people
I think you could implement personSort() more simply like this: return $a->age - $b->age;
Don Kirkby
hehe, really? i was just so accustomed to doing it like that I didn't really mathematically think it out.
meder
+10  A: 

I tried a usort, and sorted 15000 Person objects in around 1.8 seconds.

As you are concerned about the inefficiency of the calls to the comparison function, I compared it with a non-recursive Quicksort implementation. This actually ran in around one third of the time, approx 0.5 seconds.

Here's my code which benchmarks the two approaches

// Non-recurive Quicksort for an array of Person objects
// adapted from http://www.algorithmist.com/index.php/Quicksort_non-recursive.php
function quickSort( &$array )
{
 $cur = 1;
 $stack[1]['l'] = 0;
 $stack[1]['r'] = count($array)-1;

 do
 {
  $l = $stack[$cur]['l'];
  $r = $stack[$cur]['r'];
  $cur--;

  do
  {
   $i = $l;
   $j = $r;
   $tmp = $array[(int)( ($l+$r)/2 )];

   // partion the array in two parts.
   // left from $tmp are with smaller values,
   // right from $tmp are with bigger ones
   do
   {
    while( $array[$i]->age < $tmp->age )
     $i++;

    while( $tmp->age < $array[$j]->age )
     $j--;

    // swap elements from the two sides
    if( $i <= $j)
    {
     $w = $array[$i];
     $array[$i] = $array[$j];
     $array[$j] = $w;

     $i++;
     $j--;
    }

   }while( $i <= $j );

 if( $i < $r )
   {
    $cur++;
    $stack[$cur]['l'] = $i;
    $stack[$cur]['r'] = $r;
   }
   $r = $j;

  }while( $l < $r );

 }while( $cur != 0 );


}


// usort() comparison function for Person objects
function personSort( $a, $b ) {
    return $a->age == $b->age ? 0 : ( $a->age > $b->age ) ? 1 : -1;
}


// simple person object    
class Person {
  var $age;
  function __construct($age) {
    $this->age = $age;
  }
}

//---------test internal usort() on 15000 Person objects------

srand(1);
$people=array();
for ($x=0; $x<15000; $x++)
{
     $people[]=new Person(rand(1,100));
}


$start=microtime(true);
usort( $people, 'personSort' );
$total=microtime(true)-$start;

echo "usort took $total\n";


//---------test custom quicksort on 15000 Person objects------

srand(1);
$people=array();
for ($x=0; $x<15000; $x++)
{
     $people[]=new Person(rand(1,100));
}


$start=microtime(true);
quickSort( $people );
$total=microtime(true)-$start;

echo "quickSort took $total\n";

An interesting suggestion was to add a __toString method to the class and use sort(), so I tried that out too. Trouble is, you must pass SORT_STRING as the second parameter to sort get it to actually call the magic method, which has the side effect of doing a string rather than numeric sort. To counter this, you need to pad the numbers with zeroes to make it sort properly. Net result was that this was slower than both usort and the custom quickSort

sort 10000 items took      1.76266698837
usort 10000 items took     1.08757710457
quickSort 10000 items took 0.320873022079

Here's the code for the sort() using __toString():

$size=10000;

class Person {
  var $age;
  function __construct($age) {
    $this->age = $age;
    $this->sortable=sprintf("%03d", $age);
  }


  public function __toString()
  {
     return $this->sortable;
  }
}

srand(1);
$people=array();
for ($x=0; $x<$size; $x++)
{
     $people[]=new Person(rand(1,100));
}


$start=microtime(true);
sort( $people, SORT_STRING);
$total=microtime(true)-$start;

echo "sort($size) took $total\n"
Paul Dixon
The most complete and correct answer.
rogeriopvl
Did you check that algorithm for correctness? You need to change `$array[$i] < $tmp` by `$array[$i]->age < $tmp->age`, `$tmp < $array[$j]` by `$tmp->age < $array[$j]->age` and `$i->age <= $j->age` by `$i <= $j`.
Gumbo
Oh, and if you want to compare both algorithms, you should run them on the same data and not just on data of the same size but complete different characteristics. Generate your people array once and sort a copy of that same data with both algorithms.
Gumbo
Well spotted, corrected.
Paul Dixon
kudos, nice answer
meder
What about the remark about testing with the same data? You are still using different test data when comparing the algorithms.
Gumbo
You also should verify that you get the same results with each function.
Gumbo
@Gumbo, I seeded the random number generator before each test so that repeated runs of the tests were the same. I added a sanity check to the result too but omitted it from this answer.
Paul Dixon
+3  A: 

I do not advice my solution in your example because it would be ugly (And I have not benchmarked it), but it works.... And depending of the need, it may help. :)

class Person
{
  public $age;

  function __construct($age)
  {
    $this->age = $age;
  }

  public function __toString()
  {
    return $this->age;
  }
}

$person1 = new Person(14);
$person2 = new Person(5);

$persons = array($person1, $person2);
asort($persons);
Toto
+1 for the idea, I benchmarked it in my answer
Paul Dixon
As you pointed out: it is slow. :)))
Toto
+1  A: 

Here’s a stable Radix Sort implementation for values 0...256:

function radixsort(&$a)
{
    $n = count($a);
    $partition = array();
    for ($slot = 0; $slot < 256; ++$slot) {
        $partition[] = array();
    }
    for ($i = 0; $i < $n; ++$i) {
        $partition[$a[$i]->age & 0xFF][] = &$a[$i];
    } 
    $i = 0;
    for ($slot = 0; $slot < 256; ++$slot) {
        for ($j = 0, $n = count($partition[$slot]); $j < $n; ++$j) {
            $a[$i++] = &$partition[$slot][$j];
        }
    }
}

This costs only O(n) since Radix Sort is a non-comparing sorting algorithm.

Gumbo
A: 

Yes. If you implement spl ArrayObject in your person object, all the normal php array functions will work properly with it.

txyoji
A: 

One observation is that if the source of the data is from a database, it's probably faster to sort using SQL than it would be within PHP. Of course this is moot if the data source is from a CSV or XML file.