Skip to content

Commit 2519014

Browse files
authored
Merge pull request cakephp#17215 from cakephp/collection-unique
Add CollectionInterface::unique()
2 parents 1295a81 + 4d44a1c commit 2519014

4 files changed

Lines changed: 107 additions & 0 deletions

File tree

src/Collection/CollectionInterface.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,19 @@ public function filter(?callable $callback = null): CollectionInterface;
9999
*/
100100
public function reject(?callable $callback = null): CollectionInterface;
101101

102+
/**
103+
* Loops through each value in the collection and returns a new collection
104+
* with only unique values based on the value returned by ``callback``.
105+
*
106+
* The callback is passed the value as the first argument and the key as the
107+
* second argument.
108+
*
109+
* @param callable $callback the method that will receive each of the elements and
110+
* returns the value used to determine uniqueness.
111+
* @return self
112+
*/
113+
public function unique(?callable $callback = null): CollectionInterface;
114+
102115
/**
103116
* Returns true if all values in this collection pass the truth test provided
104117
* in the callback.

src/Collection/CollectionTrait.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Cake\Collection\Iterator\StoppableIterator;
3030
use Cake\Collection\Iterator\TreeIterator;
3131
use Cake\Collection\Iterator\UnfoldIterator;
32+
use Cake\Collection\Iterator\UniqueIterator;
3233
use Cake\Collection\Iterator\ZipIterator;
3334
use Countable;
3435
use InvalidArgumentException;
@@ -99,6 +100,18 @@ public function reject(?callable $callback = null): CollectionInterface
99100
return new FilterIterator($this->unwrap(), fn ($value, $key, $items) => !$callback($value, $key, $items));
100101
}
101102

103+
/**
104+
* @inheritDoc
105+
*/
106+
public function unique(?callable $callback = null): CollectionInterface
107+
{
108+
$callback ??= function ($v) {
109+
return $v;
110+
};
111+
112+
return new UniqueIterator($this->unwrap(), $callback);
113+
}
114+
102115
/**
103116
* @inheritDoc
104117
*/
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6+
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7+
*
8+
* Licensed under The MIT License
9+
* For full copyright and license information, please see the LICENSE.txt
10+
* Redistributions of files must retain the above copyright notice.
11+
*
12+
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
13+
* @link https://cakephp.org CakePHP(tm) Project
14+
* @since 5.0.0
15+
* @license https://opensource.org/licenses/mit-license.php MIT License
16+
*/
17+
namespace Cake\Collection\Iterator;
18+
19+
use Cake\Collection\Collection;
20+
use Iterator;
21+
22+
/**
23+
* Creates a filtered iterator from another iterator. The filtering is done by
24+
* passing a callback function to each of the elements and taking them out if
25+
* the value returned is not unique.
26+
*/
27+
class UniqueIterator extends Collection
28+
{
29+
/**
30+
* Creates a filtered iterator using the callback to determine which items are
31+
* accepted or rejected.
32+
*
33+
* The callback is passed the value as the first argument and the key as the
34+
* second argument.
35+
*
36+
* @param iterable $items The items to be filtered.
37+
* @param callable $callback Callback.
38+
*/
39+
public function __construct(iterable $items, callable $callback)
40+
{
41+
if (!$items instanceof Iterator) {
42+
$items = new Collection($items);
43+
}
44+
45+
$unique = [];
46+
$uniqueValues = [];
47+
foreach ($items as $k => $v) {
48+
$compareValue = $callback($v, $k);
49+
if (!in_array($compareValue, $uniqueValues, true)) {
50+
$unique[$k] = $v;
51+
$uniqueValues[] = $compareValue;
52+
}
53+
}
54+
55+
parent::__construct($unique);
56+
}
57+
}

tests/TestCase/Collection/CollectionTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,30 @@ public function testReject(): void
295295
$this->assertEquals(['a' => 1, 'b' => 2], iterator_to_array($result));
296296
}
297297

298+
public function testUnique(): void
299+
{
300+
$collection = new Collection([]);
301+
$result = $collection->unique();
302+
$this->assertSame([], iterator_to_array($result));
303+
$this->assertInstanceOf('Cake\Collection\Collection', $result);
304+
305+
$items = ['a' => 1, 'b' => 2, 'c' => 3];
306+
$collection = new Collection($items);
307+
$result = $collection->unique();
308+
$this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3], iterator_to_array($result));
309+
310+
$items = ['a' => 1, 'b' => 2, 'c' => 1, 'd' => 2, 'e' => 1, 'f' => 3];
311+
$collection = new Collection($items);
312+
$result = $collection->unique();
313+
$this->assertEquals(['a' => 1, 'b' => 2, 'f' => 3], iterator_to_array($result));
314+
315+
$result = $collection->unique(fn ($v) => (string)$v);
316+
$this->assertEquals(['a' => 1, 'b' => 2, 'f' => 3], iterator_to_array($result));
317+
318+
$result = $collection->unique(fn ($v, $k) => $k);
319+
$this->assertEquals(['a' => 1, 'b' => 2, 'c' => 1, 'd' => 2, 'e' => 1, 'f' => 3], iterator_to_array($result));
320+
}
321+
298322
/**
299323
* Tests every when the callback returns true for all elements
300324
*/

0 commit comments

Comments
 (0)