Skip to content

Commit

Permalink
refactor: Upgrade Duplicate operation.
Browse files Browse the repository at this point in the history
  • Loading branch information
drupol committed Aug 1, 2021
1 parent 3c6bc84 commit 3db3965
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 34 deletions.
25 changes: 13 additions & 12 deletions docs/pages/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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'); // []
Expand Down Expand Up @@ -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
~~~~~~
Expand Down
105 changes: 105 additions & 0 deletions docs/pages/code/operations/duplicate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

/**
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

declare(strict_types=1);

namespace App;

use Closure;
use loophp\collection\Collection;

include __DIR__ . '/../../../../vendor/autoload.php';

// Example 1 -> 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<foo>]

// 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<foo>]

// 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<booba>]
49 changes: 44 additions & 5 deletions spec/loophp/collection/CollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()]);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Contract/Operation/Duplicateable.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ interface Duplicateable
*
* @return Collection<TKey, T>
*/
public function duplicate(): Collection;
public function duplicate(?callable $comparatorCallback = null, ?callable $accessorCallback = null): Collection;
}
51 changes: 37 additions & 14 deletions src/Operation/Duplicate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<TKey, T>): Generator<TKey, T>
* @template U
*
* @return Closure(callable(U): Closure(U): bool): Closure(callable(T, TKey): U): Closure(Iterator<TKey, T>): Generator<TKey, T>
*/
public function __invoke(): Closure
{
return
/**
* @param Iterator<TKey, T> $iterator
* @param callable(U): (Closure(U): bool) $comparatorCallback
*
* @return Generator<TKey, T>
* @return Closure(callable(T, TKey): U): Closure(Iterator<TKey, T>): Generator<TKey, T>
*/
static function (Iterator $iterator): Generator {
$stack = [];
static fn (callable $comparatorCallback): Closure =>
/**
* @param callable(T, TKey): U $accessorCallback
*
* @return Closure(Iterator<TKey, T>): Generator<TKey, T>
*/
static fn (callable $accessorCallback): Closure =>
/**
* @param Iterator<TKey, T> $iterator
*
* @return Generator<TKey, T>
*/
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];
}
};
}
}

0 comments on commit 3db3965

Please sign in to comment.