diff --git a/Cake/Test/TestCase/Utility/Iterator/SortIteratorTest.php b/Cake/Test/TestCase/Utility/Iterator/SortIteratorTest.php new file mode 100644 index 00000000000..8f0bb4b5567 --- /dev/null +++ b/Cake/Test/TestCase/Utility/Iterator/SortIteratorTest.php @@ -0,0 +1,135 @@ +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'); + } + +} diff --git a/Cake/Utility/Iterator/SortIterator.php b/Cake/Utility/Iterator/SortIterator.php new file mode 100644 index 00000000000..74b2098aafa --- /dev/null +++ b/Cake/Utility/Iterator/SortIterator.php @@ -0,0 +1,137 @@ +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); + } + } + +}