Skip to content

Commit

Permalink
feat: Add Find operation (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
Radiergummi authored Oct 26, 2021
1 parent b91405e commit c2189f4
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 0 deletions.
22 changes: 22 additions & 0 deletions docs/pages/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,27 @@ Signature: ``Collection::filter(callable ...$callbacks): Collection;``
.. literalinclude:: code/operations/filter.php
:language: php

find
~~~~

Find a collection item using one or more callbacks. If the value cannot be found, that is, no callback returns true for
any collection item, it will return the ``$default`` value.

.. warning:: The ``callbacks`` parameter is variadic and will be evaluated as a logical ``OR``.
If you're looking for a logical ``AND``, you have to make multiple calls to the
same operation.

.. tip:: This operation is a shortcut for ``filter`` + ``current``.

.. tip:: It is only when the callback returns ``true`` that the value is selected.

Interface: `Findable`_

Signature: ``Collection::find($default = null, callable ...$callbacks);``

.. literalinclude:: code/operations/find.php
:language: php

first
~~~~~

Expand Down Expand Up @@ -2509,6 +2530,7 @@ Signature: ``Collection::zip(iterable ...$iterables): Collection;``
.. _Explodeable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Explodeable.php
.. _Falsyable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Falsyable.php
.. _Filterable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Filterable.php
.. _Findable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Findable.php
.. _Firstable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Firstable.php
.. _FlatMapable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/FlatMapable.php
.. _Flattenable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Flattenable.php
Expand Down
47 changes: 47 additions & 0 deletions docs/pages/code/operations/find.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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 loophp\collection\Collection;

use function range;

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

$divisibleBy3 = static fn ($value): bool => 0 === $value % 3;

// Example 1: find a value and use the default `null` if not found
$value = Collection::fromIterable(range(1, 10))
->find(null, $divisibleBy3); // 3

$value = Collection::fromIterable([1, 2, 4])
->find(null, $divisibleBy3); // null

$value = Collection::fromIterable(['foo' => 'f', 'bar' => 'b'])
->find(null, static fn ($value): bool => 'b' === $value); // 'b'

$value = Collection::fromIterable(['foo' => 'f', 'bar' => 'b'])
->find(null, static fn ($value): bool => 'x' === $value); // null

// Example 2: find a value and use a custom default if not found
$value = Collection::fromIterable([1, 2, 4])
->find(-1, $divisibleBy3); // -1

$value = Collection::fromIterable(['foo' => 'f', 'bar' => 'b'])
->find(404, static fn ($value): bool => 'x' === $value); // 404

// Example 3: use with a Doctrine Query
/** @var EntityManagerInterface $em */
$q = $em->createQuery('SELECT u FROM MyProject\Model\Product p');

$isBook = static fn ($product): bool => 'books' === $product->getCategory();

$value = Collection::fromIterable($q->toIterable())
->find(null, $isBook);
23 changes: 23 additions & 0 deletions spec/loophp/collection/CollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,29 @@ public function it_can_filter(): void
->shouldIterateAs([true]);
}

public function it_can_find(): void
{
$this::fromIterable(['foo' => 'a', 'bar' => 'b'])
->find('missing', static fn ($value): bool => 'b' === $value)
->shouldReturn('b');

$this::fromIterable(['foo' => 'a', 'bar' => 'b'])
->find('missing', static fn ($value): bool => 'd' === $value)
->shouldReturn('missing');

$this::fromIterable([1, 3, 5])
->find(null, static fn ($value): bool => $value % 2 === 0)
->shouldBeNull();

$this::fromIterable([1, 3, 5])
->find(-1, static fn ($value): bool => $value % 2 === 0)
->shouldReturn(-1);

$this::fromIterable([1, 3, 5])
->find(null, static fn ($value): bool => $value % 2 !== 0)
->shouldReturn(1);
}

public function it_can_flatMap(): void
{
$this::fromIterable([1, 2, 3])
Expand Down
6 changes: 6 additions & 0 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
use loophp\collection\Operation\Explode;
use loophp\collection\Operation\Falsy;
use loophp\collection\Operation\Filter;
use loophp\collection\Operation\Find;
use loophp\collection\Operation\First;
use loophp\collection\Operation\FlatMap;
use loophp\collection\Operation\Flatten;
Expand Down Expand Up @@ -393,6 +394,11 @@ public function filter(callable ...$callbacks): CollectionInterface
return new self(Filter::of()(...$callbacks), [$this->getIterator()]);
}

public function find($default = null, callable ...$callbacks)
{
return (new self(Find::of()($default)(...$callbacks), [$this->getIterator()]))->getIterator()->current();
}

public function first(): CollectionInterface
{
return new self(First::of(), [$this->getIterator()]);
Expand Down
3 changes: 3 additions & 0 deletions src/Contract/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use loophp\collection\Contract\Operation\Explodeable;
use loophp\collection\Contract\Operation\Falsyable;
use loophp\collection\Contract\Operation\Filterable;
use loophp\collection\Contract\Operation\Findable;
use loophp\collection\Contract\Operation\Firstable;
use loophp\collection\Contract\Operation\FlatMapable;
use loophp\collection\Contract\Operation\Flattenable;
Expand Down Expand Up @@ -164,6 +165,7 @@
* @template-extends Explodeable<TKey, T>
* @template-extends Falsyable<TKey, T>
* @template-extends Filterable<TKey, T>
* @template-extends Findable<TKey, T>
* @template-extends Firstable<TKey, T>
* @template-extends FlatMapable<TKey, T>
* @template-extends Flattenable<TKey, T>
Expand Down Expand Up @@ -280,6 +282,7 @@ interface Collection extends
Explodeable,
Falsyable,
Filterable,
Findable,
Firstable,
FlatMapable,
Flattenable,
Expand Down
34 changes: 34 additions & 0 deletions src/Contract/Operation/Findable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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 loophp\collection\Contract\Operation;

use Iterator;

/**
* @template T
* @template TKey
*/
interface Findable
{
/**
* Find a value in the collection that matches all predicates or return the
* default value.
*
* @see https://loophp-collection.readthedocs.io/en/stable/pages/api.html#find
*
* @template V
*
* @param V $default
* @param (callable(T=, TKey=, Iterator<TKey, T>=): bool) ...$callbacks
*
* @return T|V
*/
public function find($default = null, callable ...$callbacks);
}
59 changes: 59 additions & 0 deletions src/Operation/Find.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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 loophp\collection\Operation;

use Closure;
use Generator;
use Iterator;

/**
* @immutable
*
* @template TKey
* @template T
*
* phpcs:disable Generic.Files.LineLength.TooLong
*/
final class Find extends AbstractOperation
{
/**
* @pure
*
* @template V
*
* @return Closure(V): Closure(callable(T=, TKey=, Iterator<TKey, T>=): bool ...): Closure(Iterator<TKey, T>): Generator<TKey, T|V>
*/
public function __invoke(): Closure
{
return
/**
* @param V $default
*
* @return Closure(callable(T=, TKey=, Iterator<TKey, T>=): bool ...): Closure(Iterator<TKey, T>): Generator<TKey, T|V>
*/
static fn ($default): Closure =>
/**
* @param callable(T=, TKey=, Iterator<TKey, T>=): bool ...$callbacks
*
* @return Closure(Iterator<TKey, T>): Generator<TKey, T|V>
*/
static function (callable ...$callbacks) use ($default): Closure {
/** @var Closure(Iterator<TKey, T>): Generator<TKey, T|V> $pipe */
$pipe = Pipe::of()(
Filter::of()(...$callbacks),
Append::of()($default),
Head::of(),
);

// Point free style.
return $pipe;
};
}
}
77 changes: 77 additions & 0 deletions tests/static-analysis/find.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?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;

function find_checkIntElement(int $value): void
{
}
function find_checkNullableInt(?int $value): void
{
}
function find_checkStringElement(string $value): void
{
}
function find_checkNullableString(?string $value): void
{
}

$intValueCallback = static fn (int $value): bool => $value % 2 === 0;
$intValueCallback2 = static fn (int $value): bool => 2 < $value;
$intKeyValueCallback1 = static fn (int $value, int $key): bool => $value % 2 === 0 && $key % 2 === 0;
$intKeyValueCallback2 = static fn (int $value, int $key): bool => 2 < $value && 2 < $key;

$stringValueCallback = static fn (string $value): bool => 'bar' === $value;
$stringValueCallback2 = static fn (string $value): bool => '' === $value;
$stringKeyValueCallback1 = static fn (string $value, string $key): bool => 'bar' !== $value && 'foo' !== $key;
$stringKeyValueCallback2 = static fn (string $value, string $key): bool => 'bar' !== $value && '' === $key;

find_checkNullableInt(Collection::fromIterable([1, 2, 3])->find(null, $intValueCallback));
find_checkNullableInt(Collection::fromIterable([1, 2, 3])->find(null, $intValueCallback, $intValueCallback2));
find_checkNullableInt(Collection::fromIterable([1, 2, 3])->find(null, $intKeyValueCallback1));
find_checkNullableInt(Collection::fromIterable([1, 2, 3])->find(null, $intKeyValueCallback1, $intKeyValueCallback2));

find_checkNullableString(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(null, $stringValueCallback));
find_checkNullableString(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(null, $stringValueCallback, $stringValueCallback2));
find_checkNullableString(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(null, $stringKeyValueCallback1));
find_checkNullableString(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(null, $stringKeyValueCallback1, $stringKeyValueCallback2));

find_checkIntElement(Collection::fromIterable([1, 2, 3])->find(-1, $intValueCallback));
find_checkStringElement(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find('not found', $stringValueCallback));

// VALID failures - `null` as default value
/** @psalm-suppress PossiblyNullArgument @phpstan-ignore-next-line */
find_checkIntElement(Collection::fromIterable([1, 2, 3])->find(null, $intValueCallback));
/** @psalm-suppress PossiblyNullArgument @phpstan-ignore-next-line */
find_checkStringElement(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(null, $stringValueCallback));

// VALID failures - default value type mismatch
/** @psalm-suppress PossiblyInvalidArgument @phpstan-ignore-next-line */
find_checkIntElement(Collection::fromIterable([1, 2, 3])->find('not integer', $intValueCallback));
/** @psalm-suppress PossiblyInvalidArgument @phpstan-ignore-next-line */
find_checkStringElement(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(-1, $stringValueCallback));

/*
PHP 8 - using named parameters and the default `null` value -> these should work fine,
but Psalm reports no issue and PHPStan is reporting an unexpected issue:
"Parameter #1 $value of function find_checkNullableInt expects int|null, (Closure)|int given."
find_checkNullableInt(Collection::fromIterable([1, 2, 3])->find(callbacks: $intValueCallback));
find_checkNullableString(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(callbacks: $stringValueCallback));
*/

/*
PHP 8 - using named parameters and the default `null` value -> these should legitimately fail,
but Psalm reports no issue and the current PHPStan failures are due to the error mentioned above.
find_checkIntElement(Collection::fromIterable([1, 2, 3])->find(callbacks: $intValueCallback));
find_checkStringElement(Collection::fromIterable(['foo' => 'a', 'bar' => 'b'])->find(callbacks: $stringValueCallback));
*/

0 comments on commit c2189f4

Please sign in to comment.