diff --git a/docs/pages/api.rst b/docs/pages/api.rst index 4572faa10..458971536 100644 --- a/docs/pages/api.rst +++ b/docs/pages/api.rst @@ -497,7 +497,7 @@ Signature: ``Collection::column($column): Collection;`` $result = Collection::fromIterable($records) ->column('first_name'); // ['John', 'Sally', 'Jane', 'Peter'] - + $result = Collection::fromIterable($records) ->column('non_existent_key'); // [] @@ -732,21 +732,22 @@ duplicate Find duplicated values from the collection. -Interface: `Duplicateable`_ +The operation has 2 optional parameters that allow you to customize precisely +how values are accessed and compared to each other. -Signature: ``Collection::duplicate(): Collection;`` +The first parameter is the comparator. This is a curried function which takes +first the left part, then the right part and then returns a boolean. -.. code-block:: php +The second parameter is the accessor. This binary function takes the value and +the key of the current iterated value and then return the value to compare. +This is useful when you want to compare objects. - // It might return duplicated values! - Collection::fromIterable(['a', 'b', 'c', 'a', 'c', 'a']) - ->duplicate(); // [3 => 'a', 4 => 'c', 5 => 'a'] +Interface: `Duplicateable`_ + +Signature: ``Collection::duplicate(?callable $comparatorCallback = null, ?callable $accessorCallback = null): Collection;`` - // Use ::distinct() and ::normalize() to get what you want. - Collection::fromIterable(['a', 'b', 'c', 'a', 'c', 'a']) - ->duplicate() - ->distinct() - ->normalize() // [0 => 'a', 1 => 'c'] +.. literalinclude:: code/operations/duplicate.php + :language: php equals ~~~~~~ diff --git a/docs/pages/code/operations/duplicate.php b/docs/pages/code/operations/duplicate.php new file mode 100644 index 000000000..dceaec816 --- /dev/null +++ b/docs/pages/code/operations/duplicate.php @@ -0,0 +1,105 @@ + Using the default callbacks, with scalar values +$collection = Collection::fromIterable(['a', 'b', 'a', 'c', 'a', 'c']) + ->duplicate(); // [2 => 'a', 4 => 'a', 5 => 'c'] + +// Example 2 -> Using a custom comparator callback, with object values +final class User +{ + private string $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function name(): string + { + return $this->name; + } +} + +$users = [ + new User('foo'), + new User('bar'), + new User('foo'), + new User('a'), +]; + +$collection = Collection::fromIterable($users) + ->distinct( + static fn (User $left): Closure => static fn (User $right): bool => $left->name() === $right->name() + ); // [2 => User] + +// Example 3 -> Using a custom accessor callback, with object values +final class Person +{ + private string $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function name(): string + { + return $this->name; + } +} + +$users = [ + new Person('foo'), + new Person('bar'), + new Person('foo'), + new Person('a'), +]; + +$collection = Collection::fromIterable($users) + ->distinct( + null, + static fn (Person $person): string => $person->name() + ); // [2 => Person] + +// Example 4 -> Using both accessor and comparator callbacks, with object values +final class Cat +{ + private string $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function name(): string + { + return $this->name; + } +} + +$users = [ + new Cat('izumi'), + new Cat('nakano'), + new Cat('booba'), + new Cat('booba'), +]; + +$collection = Collection::fromIterable($users) + ->distinct( + static fn (string $left): Closure => static fn (string $right): bool => $left === $right, + static fn (Cat $cat): string => $cat->name() + ); // [3 => Cat] diff --git a/spec/loophp/collection/CollectionSpec.php b/spec/loophp/collection/CollectionSpec.php index bd5d31c6a..e50f6770f 100644 --- a/spec/loophp/collection/CollectionSpec.php +++ b/spec/loophp/collection/CollectionSpec.php @@ -1097,15 +1097,54 @@ public function it_can_dump(): void public function it_can_duplicate(): void { - $result = static function () { - yield 3 => 'a'; + $this::fromIterable(['a', 'b', 'c', 'a', 'c']) + ->duplicate() + ->shouldIterateAs([3 => 'a', 4 => 'c']); + + $cat = static fn (string $name) => new class($name) { + private string $name; + + public function __construct(string $name) + { + $this->name = $name; + } - yield 4 => 'c'; + public function name(): string + { + return $this->name; + } }; - $this::fromIterable(['a', 'b', 'c', 'a', 'c']) + $cats = [ + $cat1 = $cat('booba'), + $cat2 = $cat('lola'), + $cat3 = $cat('lalee'), + $cat3, + ]; + + $this::fromIterable($cats) ->duplicate() - ->shouldIterateAs($result()); + ->shouldIterateAs([3 => $cat3]); + + $this::fromIterable($cats) + ->duplicate( + static fn (object $left) => static fn (object $right) => $left->name() === $right->name() + ) + ->shouldIterateAs([3 => $cat3]); + + $this::fromIterable($cats) + ->duplicate( + static fn (string $left) => static fn (string $right) => $left === $right, + static fn (object $cat): string => $cat->name() + ) + ->shouldIterateAs([3 => $cat3]); + + $this::fromIterable($cats) + ->duplicate( + null, + static fn (object $cat): string => $cat->name() + ) + ->shouldIterateAs([3 => $cat3]); } public function it_can_equals(): void diff --git a/src/Collection.php b/src/Collection.php index 7a6e48b70..5acce6bc2 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -326,9 +326,30 @@ public function dump(string $name = '', int $size = 1, ?Closure $closure = null) return new self(Dump::of()($name)($size)($closure), [$this->getIterator()]); } - public function duplicate(): CollectionInterface + public function duplicate(?callable $comparatorCallback = null, ?callable $accessorCallback = null): CollectionInterface { - return new self(Duplicate::of(), [$this->getIterator()]); + $accessorCallback ??= + /** + * @param T $value + * @param TKey $key + * + * @return T + */ + static fn ($value, $key) => $value; + + $comparatorCallback ??= + /** + * @param T $left + * + * @return Closure(T): bool + */ + static fn ($left): Closure => + /** + * @param T $right + */ + static fn ($right): bool => $left === $right; + + return new self(Duplicate::of()($comparatorCallback)($accessorCallback), [$this->getIterator()]); } /** diff --git a/src/Contract/Operation/Duplicateable.php b/src/Contract/Operation/Duplicateable.php index 5c2e07112..8558854d1 100644 --- a/src/Contract/Operation/Duplicateable.php +++ b/src/Contract/Operation/Duplicateable.php @@ -24,5 +24,5 @@ interface Duplicateable * * @return Collection */ - public function duplicate(): Collection; + public function duplicate(?callable $comparatorCallback = null, ?callable $accessorCallback = null): Collection; } diff --git a/src/Operation/Duplicate.php b/src/Operation/Duplicate.php index 2eb1c5dee..834b14944 100644 --- a/src/Operation/Duplicate.php +++ b/src/Operation/Duplicate.php @@ -9,43 +9,66 @@ namespace loophp\collection\Operation; +use CachingIterator; use Closure; use Generator; use Iterator; - -use function in_array; +use stdClass; /** * @immutable * * @template TKey * @template T + * + * phpcs:disable Generic.Files.LineLength.TooLong */ final class Duplicate extends AbstractOperation { /** * @pure * - * @return Closure(Iterator): Generator + * @template U + * + * @return Closure(callable(U): Closure(U): bool): Closure(callable(T, TKey): U): Closure(Iterator): Generator */ public function __invoke(): Closure { return /** - * @param Iterator $iterator + * @param callable(U): (Closure(U): bool) $comparatorCallback * - * @return Generator + * @return Closure(callable(T, TKey): U): Closure(Iterator): Generator */ - static function (Iterator $iterator): Generator { - $stack = []; + static fn (callable $comparatorCallback): Closure => + /** + * @param callable(T, TKey): U $accessorCallback + * + * @return Closure(Iterator): Generator + */ + static fn (callable $accessorCallback): Closure => + /** + * @param Iterator $iterator + * + * @return Generator + */ + static function (Iterator $iterator) use ($comparatorCallback, $accessorCallback): Generator { + // Todo: Find a way to rewrite this using other operations, without side effect. + $stack = []; + + foreach ($iterator as $key => $value) { + $comparator = $comparatorCallback($accessorCallback($value, $key)); + + foreach ($stack as $item) { + if (true === $comparator($accessorCallback($item[1], $item[0]))) { + yield $key => $value; - foreach ($iterator as $key => $value) { - if (true === in_array($value, $stack, true)) { - yield $key => $value; - } + continue 2; + } + } - $stack[] = $value; - } - }; + $stack[] = [$key, $value]; + } + }; } }