Browse Source

Add CollectionInterface::unique()

Corey Taylor 2 years ago
parent
commit
4d44a1c823

+ 13 - 0
src/Collection/CollectionInterface.php

@@ -99,6 +99,19 @@ interface CollectionInterface extends Iterator, JsonSerializable, Countable
     public function reject(callable $callback): CollectionInterface;
 
     /**
+     * Loops through each value in the collection and returns a new collection
+     * with only unique values based on the value returned by ``callback``.
+     *
+     * The callback is passed the value as the first argument and the key as the
+     * second argument.
+     *
+     * @param callable $callback the method that will receive each of the elements and
+     * returns the value used to determine uniqueness.
+     * @return self
+     */
+    public function unique(?callable $callback = null): CollectionInterface;
+
+    /**
      * Returns true if all values in this collection pass the truth test provided
      * in the callback.
      *

+ 13 - 0
src/Collection/CollectionTrait.php

@@ -29,6 +29,7 @@ use Cake\Collection\Iterator\SortIterator;
 use Cake\Collection\Iterator\StoppableIterator;
 use Cake\Collection\Iterator\TreeIterator;
 use Cake\Collection\Iterator\UnfoldIterator;
+use Cake\Collection\Iterator\UniqueIterator;
 use Cake\Collection\Iterator\ZipIterator;
 use Countable;
 use InvalidArgumentException;
@@ -98,6 +99,18 @@ trait CollectionTrait
     /**
      * @inheritDoc
      */
+    public function unique(?callable $callback = null): CollectionInterface
+    {
+        $callback ??= function ($v) {
+            return $v;
+        };
+
+        return new UniqueIterator($this->unwrap(), $callback);
+    }
+
+    /**
+     * @inheritDoc
+     */
     public function every(callable $callback): bool
     {
         foreach ($this->optimizeUnwrap() as $key => $value) {

+ 57 - 0
src/Collection/Iterator/UniqueIterator.php

@@ -0,0 +1,57 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         5.0.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Collection\Iterator;
+
+use Cake\Collection\Collection;
+use Iterator;
+
+/**
+ * Creates a filtered iterator from another iterator. The filtering is done by
+ * passing a callback function to each of the elements and taking them out if
+ * the value returned is not unique.
+ */
+class UniqueIterator extends Collection
+{
+    /**
+     * Creates a filtered iterator using the callback to determine which items are
+     * accepted or rejected.
+     *
+     * The callback is passed the value as the first argument and the key as the
+     * second argument.
+     *
+     * @param iterable $items The items to be filtered.
+     * @param callable $callback Callback.
+     */
+    public function __construct(iterable $items, callable $callback)
+    {
+        if (!$items instanceof Iterator) {
+            $items = new Collection($items);
+        }
+
+        $unique = [];
+        $uniqueValues = [];
+        foreach ($items as $k => $v) {
+            $compareValue = $callback($v, $k);
+            if (!in_array($compareValue, $uniqueValues, true)) {
+                $unique[$k] = $v;
+                $uniqueValues[] = $compareValue;
+            }
+        }
+
+        parent::__construct($unique);
+    }
+}

+ 24 - 0
tests/TestCase/Collection/CollectionTest.php

@@ -291,6 +291,30 @@ class CollectionTest extends TestCase
         $this->assertInstanceOf('Cake\Collection\Collection', $result);
     }
 
+    public function testUnique(): void
+    {
+        $collection = new Collection([]);
+        $result = $collection->unique();
+        $this->assertSame([], iterator_to_array($result));
+        $this->assertInstanceOf('Cake\Collection\Collection', $result);
+
+        $items = ['a' => 1, 'b' => 2, 'c' => 3];
+        $collection = new Collection($items);
+        $result = $collection->unique();
+        $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3], iterator_to_array($result));
+
+        $items = ['a' => 1, 'b' => 2, 'c' => 1, 'd' => 2, 'e' => 1, 'f' => 3];
+        $collection = new Collection($items);
+        $result = $collection->unique();
+        $this->assertEquals(['a' => 1, 'b' => 2, 'f' => 3], iterator_to_array($result));
+
+        $result = $collection->unique(fn ($v) => (string)$v);
+        $this->assertEquals(['a' => 1, 'b' => 2, 'f' => 3], iterator_to_array($result));
+
+        $result = $collection->unique(fn ($v, $k) => $k);
+        $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 1, 'd' => 2, 'e' => 1, 'f' => 3], iterator_to_array($result));
+    }
+
     /**
      * Tests every when the callback returns true for all elements
      */