Skip to content

Commit

Permalink
Merge pull request #29 from samsonasik/v2-filter-early
Browse files Browse the repository at this point in the history
 Version 2: 🚀 Faster process with early validate filter before loop
  • Loading branch information
samsonasik authored Dec 28, 2024
2 parents 3029f92 + 29ea948 commit 7f4f836
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 24 deletions.
9 changes: 6 additions & 3 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\TypeDeclaration\Rector\ClassMethod\BoolReturnTypeFromBooleanStrictReturnsRector;

return RectorConfig::configure()
->withPhpSets(php81: true)
Expand All @@ -15,8 +15,11 @@
privatization: true,
typeDeclarations: true
)
->withSets([
PHPUnitSetList::PHPUNIT_100,
->withComposerBased(phpunit: true)
->withSkip([
BoolReturnTypeFromBooleanStrictReturnsRector::class => [
__DIR__ . '/tests/FilterTest.php',
],
])
->withParallel()
->withRootFiles()
Expand Down
45 changes: 45 additions & 0 deletions src/Assert/Filter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace ArrayLookup\Assert;

use Closure;
use InvalidArgumentException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionNamedType;

use function gettype;
use function is_object;
use function sprintf;

final class Filter
{
public static function boolean(callable $filter): void
{
if ($filter instanceof Closure) {
$reflection = new ReflectionFunction($filter);
} elseif (is_object($filter)) {
$reflection = new ReflectionMethod($filter, '__invoke');
} else {
throw new InvalidArgumentException(
sprintf('Expected Closure or invokable object, %s given', gettype($filter))
);
}

$returnType = $reflection->getReturnType();

if (! $returnType instanceof ReflectionNamedType) {
throw new InvalidArgumentException('Expected a bool return type on callable filter, null given');
}

$returnTypeName = $returnType->getName();
if ($returnTypeName !== 'bool') {
throw new InvalidArgumentException(sprintf(
'Expected a bool return type on callable filter, %s given',
$returnTypeName
));
}
}
}
6 changes: 3 additions & 3 deletions src/AtLeast.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ArrayLookup;

use ArrayLookup\Assert\Filter;
use Traversable;
use Webmozart\Assert\Assert;

Expand Down Expand Up @@ -47,14 +48,13 @@ private static function atLeastFoundTimes(
): bool {
// usage must be higher than 0
Assert::greaterThan($maxCount, 0);
// filter must be a callable with bool return type
Filter::boolean($filter);

$totalFound = 0;
foreach ($data as $key => $datum) {
$isFound = $filter($datum, $key);

// returns of callable must be bool
Assert::boolean($isFound);

if (! $isFound) {
continue;
}
Expand Down
15 changes: 9 additions & 6 deletions src/Collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ArrayLookup;

use ArrayLookup\Assert\Filter;
use Traversable;
use Webmozart\Assert\Assert;

Expand Down Expand Up @@ -67,20 +68,22 @@ public function getResults(): array
// ensure transform property is set early ->withTransform() method
Assert::isCallable($this->transform);

$count = 0;
$collectedData = [];
$isCallableWhen = is_callable($this->when);
$count = 0;
$collectedData = [];

if (is_callable($this->when)) {
// filter must be a callable with bool return type
Filter::boolean($this->when);
}

foreach ($this->data as $key => $datum) {
if ($isCallableWhen) {
if ($this->when !== null) {
/**
* @var callable(mixed $datum, int|string|null $key): bool $when
*/
$when = $this->when;
$isFound = ($when)($datum, $key);

Assert::boolean($isFound);

if (! $isFound) {
continue;
}
Expand Down
19 changes: 10 additions & 9 deletions src/Finder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace ArrayLookup;

use ArrayIterator;
use ArrayLookup\Assert\Filter;
use ArrayObject;
use Traversable;
use Webmozart\Assert\Assert;
Expand All @@ -24,12 +25,12 @@ final class Finder
*/
public static function first(iterable $data, callable $filter, bool $returnKey = false): mixed
{
// filter must be a callable with bool return type
Filter::boolean($filter);

foreach ($data as $key => $datum) {
$isFound = $filter($datum, $key);

// returns of callable must be bool
Assert::boolean($isFound);

if (! $isFound) {
continue;
}
Expand Down Expand Up @@ -71,6 +72,9 @@ public static function last(
// ensure data is array for end(), key(), current(), prev() usage
Assert::isArray($data);

// filter must be a callable with bool return type
Filter::boolean($filter);

// Use end(), key(), current(), prev() usage instead of array_reverse()
// to avoid immediatelly got "Out of memory" on many data
// see https://3v4l.org/IHo2H vs https://3v4l.org/Wqejc
Expand All @@ -91,9 +95,6 @@ public static function last(
$current = current($data);
$isFound = $filter($current, $key);

// returns of callable must be bool
Assert::boolean($isFound);

if (! $isFound) {
// go to previous row
prev($data);
Expand Down Expand Up @@ -133,12 +134,12 @@ public static function rows(
$newKey = 0;
$totalFound = 0;

// filter must be a callable with bool return type
Filter::boolean($filter);

foreach ($data as $key => $datum) {
$isFound = $filter($datum, $key);

// returns of callable must be bool
Assert::boolean($isFound);

if (! $isFound) {
continue;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Only.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ArrayLookup;

use ArrayLookup\Assert\Filter;
use Traversable;
use Webmozart\Assert\Assert;

Expand Down Expand Up @@ -47,14 +48,13 @@ private static function onlyFoundTimes(
): bool {
// usage must be higher than 0
Assert::greaterThan($maxCount, 0);
// filter must be a callable with bool return type
Filter::boolean($filter);

$totalFound = 0;
foreach ($data as $key => $datum) {
$isFound = $filter($datum, $key);

// returns of callable must be bool
Assert::boolean($isFound);

if (! $isFound) {
continue;
}
Expand Down
71 changes: 71 additions & 0 deletions tests/FilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace ArrayLookup\Tests;

use ArrayLookup\AtLeast;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

final class FilterTest extends TestCase
{
public function testOnceWithFilterInvokableClass(): void
{
$data = [1, 2, 3];
$filter = new class {
public function __invoke(int $datum): bool
{
return $datum === 1;
}
};

$this->assertTrue(AtLeast::once($data, $filter));
}

public function testOnceWithStringFilter(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected Closure or invokable object, string given');

$data = [1, 'f'];
$filter = 'is_string';

AtLeast::once($data, $filter);
}

public function testWithoutReturnTypeCallable(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected a bool return type on callable filter, null given');

$data = [1, 2, 3];

// phpcs:disable
$filter = new class {
public function __invoke(int $datum)
{
return $datum === 1;
}
};
// phpcs:enable

AtLeast::once($data, $filter);
}

public function testWithNonBoolReturnTypeCallable(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Expected a bool return type on callable filter, string given');

$data = [1, 2, 3];
$filter = new class {
public function __invoke(int $datum): string
{
return 'test';
}
};

AtLeast::once($data, $filter);
}
}

0 comments on commit 7f4f836

Please sign in to comment.