Skip to content

Commit

Permalink
refactor: Update distinct operation.
Browse files Browse the repository at this point in the history
  • Loading branch information
drupol committed Jun 23, 2021
1 parent 9f13526 commit 023d399
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 40 deletions.
18 changes: 13 additions & 5 deletions docs/pages/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -554,14 +554,22 @@ distinct

Remove duplicated values from a collection, preserving keys.

Interface: `Distinctable`_
The operation has 2 optional parameters that allow you to customize precisely
how values are accessed and compared to each other.

Signature: ``Collection::distinct();``
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 take 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.

Interface: `Distinctable`_

Signature: ``Collection::distinct(?callable $comparatorCallback = null, ?callable $accessorCallback = null);``

$collection = Collection::fromIterable(['a', 'b', 'a', 'c'])
->distinct(); // [0 => 'a', 1 => 'b', 3 => 'c']
.. literalinclude:: code/operations/distinct.php
:language: php

drop
~~~~
Expand Down
76 changes: 76 additions & 0 deletions docs/pages/code/operations/distinct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?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 callback, with scalar values
$collection = Collection::fromIterable(['a', 'b', 'a', 'c'])
->distinct(); // [0 => 'a', 1 => 'b', 3 => 'c']

// Example 2 -> Using one custom 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()
); // [0 => User<foo>, 1 => User<bar>, 3 => User<a>]

// Example 3 -> Using two custom 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) => $cat->name()
); // [0 => Cat<izumi>, 1 => Cat<nakano>, 2 => Cat<booba>]
45 changes: 45 additions & 0 deletions spec/loophp/collection/CollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,51 @@ public function it_can_distinct(): void
$this::fromIterable([1, 1, 2, 2, 3, 3, $stdclass, $stdclass])
->distinct()
->shouldIterateAs([0 => 1, 2 => 2, 4 => 3, 6 => $stdclass]);

$cat = static fn (string $name) => new class($name) {
private string $name;

public function __construct(string $name)
{
$this->name = $name;
}

public function name(): string
{
return $this->name;
}
};

$cats = [
$cat1 = $cat('izumi'),
$cat2 = $cat('nakano'),
$cat3 = $cat('booba'),
$cat3,
];

$this::fromIterable($cats)
->distinct()
->shouldIterateAs([$cat1, $cat2, $cat3]);

$this::fromIterable($cats)
->distinct(
static fn ($left) => static fn ($right) => $left->name() === $right->name()
)
->shouldIterateAs([$cat1, $cat2, $cat3]);

$this::fromIterable($cats)
->distinct(
static fn ($left) => static fn ($right) => $left === $right,
static fn ($cat): string => $cat->name()
)
->shouldIterateAs([$cat1, $cat2, $cat3]);

$this::fromIterable($cats)
->distinct(
null,
static fn ($cat): string => $cat->name()
)
->shouldIterateAs([$cat1, $cat2, $cat3]);
}

public function it_can_do_the_cartesian_product(): void
Expand Down
25 changes: 23 additions & 2 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,30 @@ public function diffKeys(...$values): CollectionInterface
return new self(DiffKeys::of()(...$values), $this->getIterator());
}

public function distinct(): CollectionInterface
public function distinct(?callable $comparatorCallback = null, ?callable $accessorCallback = null): CollectionInterface
{
return new self(Distinct::of(), $this->getIterator());
$accessorCallback ??=
/**
* @param T $value
* @param TKey $key
*
* @return T
*/
static fn ($value, $key) => $value;

$comparatorCallback ??=
/**
* @param mixed $left
*
* @return Closure(mixed): bool
*/
static fn ($left): Closure =>
/**
* @param mixed $right
*/
static fn ($right): bool => $left === $right;

return new self(Distinct::of()($comparatorCallback)($accessorCallback), $this->getIterator());
}

public function drop(int ...$counts): CollectionInterface
Expand Down
6 changes: 5 additions & 1 deletion src/Contract/Operation/Distinctable.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace loophp\collection\Contract\Operation;

use Closure;
use loophp\collection\Contract\Collection;

/**
Expand All @@ -18,7 +19,10 @@
interface Distinctable
{
/**
* @param null|callable(mixed): (Closure(mixed): bool) $comparatorCallback
* @param null|callable(T, TKey): mixed $accessorCallback
*
* @return Collection<TKey, T>
*/
public function distinct(): Collection;
public function distinct(?callable $comparatorCallback = null, ?callable $accessorCallback = null): Collection;
}
82 changes: 50 additions & 32 deletions src/Operation/Distinct.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,64 @@
/**
* @template TKey
* @template T
*
* phpcs:disable Generic.Files.LineLength.TooLong
*/
final class Distinct extends AbstractOperation
{
/**
* @return Closure(Iterator<TKey, T>): Generator<TKey, T>
* @return Closure(callable(mixed): (Closure(mixed): bool)): Closure(callable(T, TKey): mixed): Closure(Iterator<TKey, T>): Generator<TKey, T>
*/
public function __invoke(): Closure
{
$foldLeftCallback =
return
/**
* @param list<array{0: TKey, 1: T}> $seen
* @param array{0: TKey, 1: T} $value
* @param callable(mixed): (Closure(mixed): bool) $comparatorCallback
*
* @return Closure(callable(T, TKey): mixed): Closure(Iterator<TKey, T>): Generator<TKey, T>
*/
static function (array $seen, array $value): array {
$isSeen = false;

foreach ($seen as $item) {
if ($item[1] === $value[1]) {
$isSeen = true;

break;
}
}

if (false === $isSeen) {
$seen[] = $value;
}

return $seen;
};

/** @var Closure(Iterator<TKey, T>): Generator<TKey, T> $pipe */
$pipe = Pipe::of()(
Pack::of(),
FoldLeft::of()($foldLeftCallback)([]),
Unwrap::of(),
Unpack::of()
);

// Point free style.
return $pipe;
static fn (callable $comparatorCallback): Closure =>
/**
* @param callable(T, TKey): mixed $accessorCallback
*
* @return Closure(Iterator<TKey, T>): Generator<TKey, T>
*/
static function (callable $accessorCallback) use ($comparatorCallback): Closure {
$foldLeftCallbackBuilder =
static fn (callable $accessorCallback): Closure => static fn (callable $comparatorCallback): Closure =>
/**
* @param list<array{0: TKey, 1: T}> $seen
* @param array{0: TKey, 1: T} $value
*/
static function (array $seen, array $value) use ($accessorCallback, $comparatorCallback): array {
$isSeen = false;
$comparator = $comparatorCallback($accessorCallback($value[1], $value[0]));

foreach ($seen as $item) {
if (true === $comparator($accessorCallback($item[1], $item[0]))) {
$isSeen = true;

break;
}
}

if (false === $isSeen) {
$seen[] = $value;
}

return $seen;
};

/** @var Closure(Iterator<TKey, T>): Generator<TKey, T> $pipe */
$pipe = Pipe::of()(
Pack::of(),
FoldLeft::of()($foldLeftCallbackBuilder($accessorCallback)($comparatorCallback))([]),
Unwrap::of(),
Unpack::of()
);

// Point free style.
return $pipe;
};
}
}
65 changes: 65 additions & 0 deletions tests/static-analysis/distinct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

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

declare(strict_types=1);

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

use loophp\collection\Collection;
use loophp\collection\Contract\Collection as CollectionInterface;

/**
* @param CollectionInterface<int, int> $collection
*/
function distinct_checkList(CollectionInterface $collection): void
{
}
/**
* @param CollectionInterface<string, string> $collection
*/
function distinct_checkMap(CollectionInterface $collection): void
{
}
function distinct_checkIntElement(int $value): void
{
}
function distinct_checkNullableInt(?int $value): void
{
}
function distinct_checkStringElement(string $value): void
{
}
function distinct_checkNullableString(?string $value): void
{
}

distinct_checkList(Collection::fromIterable([1, 2, 3, 1])->distinct());
distinct_checkMap(Collection::fromIterable(['a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'bar'])->distinct());

distinct_checkList(Collection::empty()->distinct());
distinct_checkMap(Collection::empty()->distinct());

distinct_checkNullableInt(Collection::fromIterable([1, 2, 3, 1])->distinct()->current());
distinct_checkNullableString(Collection::fromIterable(['foo' => 'bar', 'baz' => 'bar'])->head()->current());

// This retrieval method doesn't cause static analysis complaints
// but is not always reliable because of that.
distinct_checkIntElement(Collection::fromIterable([1, 2, 3, 1])->distinct()->all()[0]);
distinct_checkStringElement(Collection::fromIterable(['a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'bar'])->distinct()->all()['a']);
distinct_checkStringElement(Collection::fromIterable(['a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'bar'])->distinct()->all()['b']);

// VALID failures - `current` returns T|null
/** @psalm-suppress PossiblyNullArgument @phpstan-ignore-next-line */
distinct_checkIntElement(Collection::fromIterable([1, 2, 3, 1])->distinct()->current());
/** @psalm-suppress PossiblyNullArgument @phpstan-ignore-next-line */
distinct_checkStringElement(Collection::fromIterable(['foo' => 'bar', 'bar'])->distinct()->current());

// VALID failures - these keys don't exist
/** @psalm-suppress InvalidArrayOffset */
distinct_checkIntElement(Collection::fromIterable([1, 2, 3, 1])->distinct()->all()[4]);
/** @psalm-suppress InvalidArrayOffset @phpstan-ignore-next-line */
distinct_checkStringElement(Collection::fromIterable(['foo' => 'bar', 'baz' => 'bar'])->distinct()->all()['plop']);

0 comments on commit 023d399

Please sign in to comment.