Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: UpdateDistinct and Duplicate operations. #168

Merged
merged 3 commits into from
Aug 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion docs/pages/code/operations/distinct.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,5 @@ public function name(): string
$collection = Collection::fromIterable($users)
->distinct(
static fn (string $left): Closure => static fn (string $right): bool => $left === $right,
static fn (Cat $cat) => $cat->name()
static fn (Cat $cat): string => $cat->name()
); // [0 => Cat<izumi>, 1 => Cat<nakano>, 2 => Cat<booba>]
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;
}
58 changes: 17 additions & 41 deletions src/Operation/Distinct.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,53 +44,29 @@ public function __invoke(): Closure
*
* @return Closure(Iterator<TKey, T>): Generator<TKey, T>
*/
static function (callable $accessorCallback) use ($comparatorCallback): Closure {
static fn (callable $accessorCallback): Closure =>
/**
* @param callable(T, TKey): U $accessorCallback
* @param Iterator<TKey, T> $iterator
*
* @return Closure(callable(U): Closure(U): bool): Closure(list<array{0: TKey, 1: T}>, array{0: TKey, 1: T}): list<array{0: TKey, 1: T}>
* @return Generator<TKey, T>
*/
$foldLeftCallbackBuilder =
static fn (callable $accessorCallback): Closure =>
/**
* @param callable(U): (Closure(U): bool) $comparatorCallback
*
* @return Closure(list<array{0: TKey, 1: T}>, array{0: TKey, 1: T}): list<array{0: TKey, 1: T}>
*/
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]));
static function (Iterator $iterator) use ($comparatorCallback, $accessorCallback): Generator {
// Todo: Find a way to rewrite this using composition of operations, without side effect.
$stack = [];

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

break;
}
}
foreach ($stack as $item) {
if (true === $comparator($accessorCallback($item[1], $item[0]))) {
continue 2;
}
}

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

return $seen;
};

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

// Point free style.
return $pipe;
};
yield $key => $value;
}
};
}
}
Loading