Skip to content

Commit

Permalink
Implemented Collection::groupBy()
Browse files Browse the repository at this point in the history
  • Loading branch information
lorenzo committed Dec 26, 2013
1 parent e0f3880 commit f6f27c7
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 10 deletions.
57 changes: 57 additions & 0 deletions Cake/Test/TestCase/Utility/CollectionTest.php
Expand Up @@ -312,4 +312,61 @@ public function testMin() {
$this->assertEquals(['a' => ['b' => ['c' => 4]]], $collection->min('a.b.c'));
}

/**
* Tests groupBy
*
* @return void
*/
public function testGroupBy() {
$items = [
['id' => 1, 'name' => 'foo', 'parent_id' => 10],
['id' => 2, 'name' => 'bar', 'parent_id' => 11],
['id' => 3, 'name' => 'baz', 'parent_id' => 10],
];
$collection = new Collection($items);
$grouped = $collection->groupBy('parent_id');
$expected = [
10 => [
['id' => 1, 'name' => 'foo', 'parent_id' => 10],
['id' => 3, 'name' => 'baz', 'parent_id' => 10],
],
11 => [
['id' => 2, 'name' => 'bar', 'parent_id' => 11],
]
];
$this->assertEquals($expected, iterator_to_array($grouped));
$this->assertInstanceOf('\Cake\Utility\Collection', $grouped);

$grouped = $collection->groupBy(function($element) {
return $element['parent_id'];
});
$this->assertEquals($expected, iterator_to_array($grouped));
}

/**
* Tests grouping by a deep key
*
* @return void
*/
public function testGroupByDeepKey() {
$items = [
['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]],
['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]],
['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]],
];
$collection = new Collection($items);
$grouped = $collection->groupBy('thing.parent_id');
$expected = [
10 => [
['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]],
['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]],
],
11 => [
['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]],
]
];
$this->assertEquals($expected, iterator_to_array($grouped));
}


}
89 changes: 88 additions & 1 deletion Cake/Utility/Collection.php
Expand Up @@ -370,7 +370,54 @@ public function sortBy($callback, $dir = SORT_DESC, $type = SORT_NUMERIC) {
return new self(new SortIterator($this, $callback, $dir, $type));
}

public function groupBy($property) {
/**
* Splits a collection into sets, grouped by the result of running each value
* through the callback. If $callback is is a string instead of a callable,
* groups by the property named by $callback on each of the values.
*
* When $callback is a string it should be a property name to extract or
* a dot separated path of properties that should be followed to get the last
* one in the path.
*
* ###Example:
*
* {{{
* $items = [
* ['id' => 1, 'name' => 'foo', 'parent_id' => 10],
* ['id' => 2, 'name' => 'bar', 'parent_id' => 11],
* ['id' => 3, 'name' => 'baz', 'parent_id' => 10],
* ];
*
* $group = (new Collection($items))->groupBy('parent_id');
*
* //Or
* $group = (new Collection($items))->groupBy(function($e) {
* return $e['parent_id'];
* });
*
* //Result will look like
* [
* 10 => [
* ['id' => 1, 'name' => 'foo', 'parent_id' => 10],
* ['id' => 3, 'name' => 'baz', 'parent_id' => 10],
* ],
* 11 => [
* ['id' => 2, 'name' => 'bar', 'parent_id' => 11],
* ]
* ];
* }}}
*
* @param callable|string the callback or column name to use for grouping
* or a function returning the grouping key out of the provided element
* @return \Cake\Utility\Collection
*/
public function groupBy($callback) {
$callback = $this->_propertyExtractor($callback);
$group = [];
foreach ($this as $value) {
$group[$callback($value)][] = $value;
}
return new self($group);
}

public function indexBy($property) {
Expand All @@ -385,4 +432,44 @@ public function shuffle() {
public function sample($size) {
}

/**
* Returns a callable that can be used to extract a property or column from
* an array or object based on a dot separated path.
*
* @param string|callable $callback A dot separated path of column to follow
* so that the final one can be returned or a callable that will take care
* of doing that.
* @return callable
*/
protected function _propertyExtractor($callback) {
if (is_string($callback)) {
$path = $path = explode('.', $callback);
$callback = function($element) use ($path) {
return $this->_extract($element, $path);
};
}

return $callback;
}

/**
* Returns a column from $data that can be extracted
* by iterating over the column names contained in $path
*
* @param array|\ArrayAccess $data
* @param array $path
* @return mixed
*/
protected function _extract($data, $path) {
$value = null;
foreach ($path as $column) {
if (!isset($data[$column])) {
return null;
}
$value = $data[$column];
$data = $value;
}
return $value;
}

}
10 changes: 1 addition & 9 deletions Cake/Utility/Iterator/ExtractIterator.php
Expand Up @@ -65,15 +65,7 @@ public function __construct($items, $path) {
*/
public function current() {
$current = parent::current();
$value = null;
foreach ($this->_path as $column) {
if (!isset($current[$column])) {
return null;
}
$value = $current[$column];
$current = $value;
}
return $value;
return $this->_extract($current, $this->_path);
}

}

0 comments on commit f6f27c7

Please sign in to comment.