Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Adding SortIterator
  • Loading branch information
lorenzo committed Dec 26, 2013
1 parent cab6f5e commit 5cbcb59
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 0 deletions.
135 changes: 135 additions & 0 deletions Cake/Test/TestCase/Utility/Iterator/SortIteratorTest.php
@@ -0,0 +1,135 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since CakePHP(tm) v 3.0.0
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace Cake\Test\TestCase\Utility\Iterator;

use ArrayObject;
use Cake\TestSuite\TestCase;
use Cake\Utility\Iterator\SortIterator;

/**
* SortIterator Test
*
*/
class SortIteratorTest extends TestCase {

/**
* Tests sorting numbers with an identity callbacks
*
* @return void
*/
public function testSortNumbersIdentity() {
$items = new ArrayObject([3, 5, 1, 2, 4]);
$identity = function($a) {
return $a;
};
$sorted = new SortIterator($items, $identity);
$expected = array_combine(range(4, 0), range(5, 1));
$this->assertEquals($expected, iterator_to_array($sorted));

$sorted = new SortIterator($items, $identity, SORT_ASC);
$expected = array_combine(range(4, 0), range(1, 5));
$this->assertEquals($expected, iterator_to_array($sorted));
}

/**
* Tests sorting numbers with custom callback
*
* @return void
*/
public function testSortNumbersCustom() {
$items = new ArrayObject([3, 5, 1, 2, 4]);
$callback = function($a) {
return sin($a);
};
$sorted = new SortIterator($items, $callback);
$expected = array_combine(range(4, 0), [2, 1, 3, 5, 4]);
$this->assertEquals($expected, iterator_to_array($sorted));

$sorted = new SortIterator($items, $callback, SORT_ASC);
$expected = array_combine(range(4, 0), [4, 5, 3, 1, 2]);
$this->assertEquals($expected, iterator_to_array($sorted));
}

/**
* Tests sorting a complex structure with numeric sort
*
* @return void
*/
public function testSortComplexNumeric() {
$items = new ArrayObject([
['foo' => 1, 'bar' => 'a'],
['foo' => 10, 'bar' => 'a'],
['foo' => 2, 'bar' => 'a'],
['foo' => 13, 'bar' => 'a'],
]);
$callback = function($a) {
return $a['foo'];
};
$sorted = new SortIterator($items, $callback, SORT_DESC, SORT_NUMERIC);
$expected = [
3 => ['foo' => 13, 'bar' => 'a'],
2 => ['foo' => 10, 'bar' => 'a'],
1 => ['foo' => 2, 'bar' => 'a'],
0 => ['foo' => 1, 'bar' => 'a'],
];
$this->assertEquals($expected, iterator_to_array($sorted));

$sorted = new SortIterator($items, $callback, SORT_ASC, SORT_NUMERIC);
$expected = [
3 => ['foo' => 1, 'bar' => 'a'],
2 => ['foo' => 2, 'bar' => 'a'],
1 => ['foo' => 10, 'bar' => 'a'],
0 => ['foo' => 13, 'bar' => 'a'],
];
$this->assertEquals($expected, iterator_to_array($sorted));
}


/**
* Tests sorting a complex structure with natural sort
*
* @return void
*/
public function testSortComplexNatural() {
$items = new ArrayObject([
['foo' => 'foo_1', 'bar' => 'a'],
['foo' => 'foo_10', 'bar' => 'a'],
['foo' => 'foo_2', 'bar' => 'a'],
['foo' => 'foo_13', 'bar' => 'a'],
]);
$callback = function($a) {
return $a['foo'];
};
$sorted = new SortIterator($items, $callback, SORT_DESC, SORT_NATURAL);
$expected = [
3 => ['foo' => 'foo_13', 'bar' => 'a'],
2 => ['foo' => 'foo_10', 'bar' => 'a'],
1 => ['foo' => 'foo_2', 'bar' => 'a'],
0 => ['foo' => 'foo_1', 'bar' => 'a'],
];
$this->assertEquals($expected, iterator_to_array($sorted));

$sorted = new SortIterator($items, $callback, SORT_ASC, SORT_NATURAL);
$expected = [
3 => ['foo' => 'foo_1', 'bar' => 'a'],
2 => ['foo' => 'foo_2', 'bar' => 'a'],
1 => ['foo' => 'foo_10', 'bar' => 'a'],
0 => ['foo' => 'foo_13', 'bar' => 'a'],
];
$this->assertEquals($expected, iterator_to_array($sorted));
$this->assertEquals($expected, iterator_to_array($sorted), 'Iterator should rewind');
}

}
137 changes: 137 additions & 0 deletions Cake/Utility/Iterator/SortIterator.php
@@ -0,0 +1,137 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since CakePHP(tm) v 3.0.0
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace Cake\Utility\Iterator;

use SplHeap;

/**
* An iterator that will return the passed items in order. The order is given by
* the value returned in a callback function that maps each of the elements.
*
* ###Example:
*
* {{{
* $items = [$user1, $user2, $user3];
* $sorted = new SortIterator($items, function($user) {
* return $user->age;
* });
*
* // output all user name order by their age in descending order
* foreach ($sorted as $user) {
* echo $user->name;
* }
* }}}
*
* This iterator does not preserve the keys passed in the original elements.
*/
class SortIterator extends SplHeap {

/**
* Original items passed to this iterator
*
* @var array|\Traversable
*/
protected $_items;

/**
* The callback used to extract the column or property from the elements
*
* @var callable
*/
protected $_callback;

/**
* The direction in which the elements should be sorted. The constants
* `SORT_ASC` and `SORT_DESC` are the accepted values
*
* @var string
*/
protected $_dir;

/**
* The type of sort comparison to perform.
*
* @var string
*/
protected $_type;

/**
* Wraps this iterator around the passed items so when iterated they are returned
* in order.
*
* The callback will receive as first argument each of the elements in $items,
* the value returned in the callback will be used as the value for sorting such
* element. Please not that the callback function could be called more than once
* per element.
*
* @param array|\Traversable $items The values to sort
* @param callable $callback A function used to return the actual value to be
* compared
* @param integer $dir either SORT_DESC or SORT_ASC
* @param integer $type the type of comparison to perform, either SORT_STRING
* SORT_NUMERIC or SORT_NATURAL
* @return void
*/
public function __construct($items = [], callable $c, $dir = SORT_DESC, $type = SORT_STRING) {
$this->_items = $items;
$this->_callback = $c;
$this->_dir = $dir;
$this->_type = $type;
}

/**
* The comparison function used to sort the elements
*
* @param mixed $a an element in the list
* @param mixed $b an element in the list
* @return integer
*/
public function compare($a, $b) {
if ($this->_dir === SORT_ASC) {
list($a, $b) = [$b, $a];
}

$callback = $this->_callback;
$a = $callback($a);
$b = $callback($b);

if ($this->_type === SORT_NUMERIC) {
return $a - $b;
}

if ($this->_type === SORT_NATURAL) {
return strnatcmp($a, $b);
}

if ($this->_type === SORT_STRING) {
return strcmp($a, $b);
}

return strcoll($a, $b);
}

/**
* SplHeap removes elements upon iteration. Implementing rewind so that
* this iterator can be reused, at least at a cost.
*
* @return void
*/
public function rewind() {
foreach ($this->_items as $item) {
$this->insert($item);
}
}

}

0 comments on commit 5cbcb59

Please sign in to comment.