From 140ee1e3417624047699c5830dbf32d69b5f44cc Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sun, 25 Jan 2026 05:38:28 +0700 Subject: [PATCH 1/3] Add AtMost::once(), Atmost::twice(), Atmost::times() as the opposite of AtLeast --- README.md | 78 +++++++++++++++++++++++- src/AtMost.php | 71 ++++++++++++++++++++++ tests/AtMostTest.php | 140 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 src/AtMost.php create mode 100644 tests/AtMostTest.php diff --git a/README.md b/README.md index 02c33d9..f06fc13 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Features -------- - [x] Verify at least times: `once()`, `twice()`, `times()` +- [x] Verify at most times: `once()`, `twice()`, `times()` - [x] Verify exact times: `once()`, `twice()`, `times()` - [x] Search data: `first()`, `last()`, `rows()`, `partition()` - [x] Collect data with filter and transform @@ -129,7 +130,78 @@ $times = 3; var_dump(AtLeast::times($data, $filter, $times)) // false ``` -**B. Only** +**B. AtMost** +--------------- + +#### 1. `AtMost::once()` + +It verify that data has filtered found item at most once. + +```php +use ArrayLookup\AtMost; + +$data = [1, 2, 3]; +$filter = static fn($datum): bool => $datum === 1; + +var_dump(AtMost::once($data, $filter)) // true + +$data = [1, "1", 3]; +$filter = static fn($datum): bool => $datum == 1; + +var_dump(AtMost::once($data, $filter)) // false + +// WITH key array included, pass $key variable as 2nd arg on filter to be used in filter + +$data = ['abc', 'def', 'some test']; +$filter = static fn(string $datum, int $key): bool => $datum === 'def' && $key === 1; + +var_dump(AtMost::once($data, $filter)) // true + +$data = ['abc', 'def', 'some test']; +$filter = static fn(string $datum, int $key): bool => $key > 0; + +var_dump(AtMost::once($data, $filter)) // false +``` + +#### 2. `AtMost::twice()` + +It verify that data has filtered found items at most twice. + +```php +use ArrayLookup\AtMost; + +$data = [1, "1", 2]; +$filter = static fn($datum): bool => $datum == 1; + +var_dump(AtMost::twice($data, $filter)) // true + +$data = [1, "1", 2, 1]; +$filter = static fn($datum): bool => $datum == 1; + +var_dump(AtMost::twice($data, $filter)) // false +``` + +#### 3. `AtMost::times()` + +It verify that data has filtered found items at most times passed in 3rd arg. + +```php +use ArrayLookup\AtMost; + +$data = [false, null, 0]; +$filter = static fn($datum): bool => ! $datum; +$times = 3; + +var_dump(AtMost::times($data, $filter, $times)) // true + +$data = [false, null, 0, 0]; +$filter = static fn($datum): bool => ! $datum; +$times = 3; + +var_dump(AtMost::times($data, $filter, $times)) // false +``` + +**C. Only** --------------- #### 1. `Only::once()` @@ -230,7 +302,7 @@ $times = 2; var_dump(Only::times($data, $filter, $times)) // false ``` -**C. Finder** +**D. Finder** --------------- #### 1. `Finder::first()` @@ -401,7 +473,7 @@ var_dump($even); // [0 => 10, 2 => 30] var_dump($odd); // [1 => 20, 3 => 40] ``` -**D. Collector** +**E. Collector** --------------- It collect filtered data, with new transformed each data found: diff --git a/src/AtMost.php b/src/AtMost.php new file mode 100644 index 0000000..d2d9bdb --- /dev/null +++ b/src/AtMost.php @@ -0,0 +1,71 @@ +|Traversable $data + * @param callable(mixed $datum, int|string|null $key): bool $filter + */ + public static function once(iterable $data, callable $filter): bool + { + return self::atMostFoundTimes($data, $filter, 1); + } + + /** + * @param array|Traversable $data + * @param callable(mixed $datum, int|string|null $key): bool $filter + */ + public static function twice(iterable $data, callable $filter): bool + { + return self::atMostFoundTimes($data, $filter, 2); + } + + /** + * @param array|Traversable $data + * @param callable(mixed $datum, int|string|null $key): bool $filter + */ + public static function times(iterable $data, callable $filter, int $count): bool + { + return self::atMostFoundTimes($data, $filter, $count); + } + + /** + * @param array|Traversable $data + * @param callable(mixed $datum, int|string|null $key): bool $filter + */ + private static function atMostFoundTimes( + iterable $data, + callable $filter, + int $maxCount + ): 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); + + if (! $isFound) { + continue; + } + + ++$totalFound; + + if ($totalFound > $maxCount) { + return false; + } + } + + return true; + } +} diff --git a/tests/AtMostTest.php b/tests/AtMostTest.php new file mode 100644 index 0000000..6ca54a9 --- /dev/null +++ b/tests/AtMostTest.php @@ -0,0 +1,140 @@ +assertSame( + $expected, + AtMost::once($data, $filter) + ); + } + + /** + * @return Iterator + */ + public static function onceDataProvider(): Iterator + { + yield [ + [1, 2, 3], + static fn($datum): bool => $datum === 4, + true, + ]; + yield [ + [1, 2, 3], + static fn($datum): bool => $datum === 1, + true, + ]; + yield [ + [1, '1', 3], + static fn($datum): bool => $datum == 1, + false, + ]; + yield [ + ['abc', 'def', 'some test'], + static fn(string $datum, int $key): bool => $datum === 'def' && $key === 1, + true, + ]; + yield [ + ['abc', 'def', 'some test'], + static fn(string $datum, int $key): bool => $key > 0, + false, + ]; + } + + /** + * @param int[]|string[] $data + */ + #[DataProvider('twiceDataProvider')] + public function testTwice(array $data, callable $filter, bool $expected): void + { + $this->assertSame( + $expected, + AtMost::twice($data, $filter) + ); + } + + /** + * @return Iterator + */ + public static function twiceDataProvider(): Iterator + { + yield [ + [1, '1', 3], + static fn($datum): bool => $datum == 1, + true, + ]; + yield [ + [1, '1', 2, 1], + static fn($datum): bool => $datum == 1, + false, + ]; + yield [ + ['abc', 'def', 'some test'], + static fn(string $datum, int $key): bool => $datum !== 'abc' && $key > 0, + true, + ]; + yield [ + ['abc', 'def', 'some test', 'another'], + static fn(string $datum, int $key): bool => $datum !== 'abc' && $key > 0, + false, + ]; + } + + /** + * @param int[]|bool[]|null[]|string[] $data + */ + #[DataProvider('timesDataProvider')] + public function testTimes(array $data, callable $filter, bool $expected): void + { + $this->assertSame( + $expected, + AtMost::times($data, $filter, 3) + ); + } + + /** + * @return Iterator + */ + public static function timesDataProvider(): Iterator + { + yield [ + [0, false, null], + static fn($datum): bool => ! $datum, + true, + ]; + yield [ + [0, false, null, 'x'], + static fn($datum): bool => ! $datum, + true, + ]; + yield [ + [0, false, null, 0], + static fn($datum): bool => ! $datum, + false, + ]; + yield [ + ['abc', 'def', 'some test', 'another test'], + static fn(string $datum, int $key): bool => $datum !== 'abc' && $key > 0, + true, + ]; + yield [ + ['abc', 'def', 'some test', 'another test', 'yet another'], + static fn(string $datum, int $key): bool => $datum !== 'abc' && $key > 0, + false, + ]; + } +} From 024718b184a708119ac0e596d84eb09d6109d7f5 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sun, 25 Jan 2026 05:41:41 +0700 Subject: [PATCH 2/3] cs fix --- tests/AtMostTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/AtMostTest.php b/tests/AtMostTest.php index 6ca54a9..82de0d8 100644 --- a/tests/AtMostTest.php +++ b/tests/AtMostTest.php @@ -23,6 +23,7 @@ public function testOnce(array $data, callable $filter, bool $expected): void ); } + // phpcs:disable /** * @return Iterator */ @@ -54,6 +55,7 @@ public static function onceDataProvider(): Iterator false, ]; } + // phpcs:enable /** * @param int[]|string[] $data @@ -67,6 +69,7 @@ public function testTwice(array $data, callable $filter, bool $expected): void ); } + // phpcs:disable /** * @return Iterator */ @@ -93,6 +96,7 @@ public static function twiceDataProvider(): Iterator false, ]; } + // phpcs:enable /** * @param int[]|bool[]|null[]|string[] $data From f3720903001fcb1680b8d044dfc0368c1f2c465a Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sun, 25 Jan 2026 05:42:42 +0700 Subject: [PATCH 3/3] re-run rector --- tests/AtMostTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/AtMostTest.php b/tests/AtMostTest.php index 82de0d8..ff9639c 100644 --- a/tests/AtMostTest.php +++ b/tests/AtMostTest.php @@ -55,6 +55,7 @@ public static function onceDataProvider(): Iterator false, ]; } + // phpcs:enable /** @@ -96,6 +97,7 @@ public static function twiceDataProvider(): Iterator false, ]; } + // phpcs:enable /**