From 1d4b031f87262e39eb3d11d0e450b7a62215e58b Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 20 Apr 2026 11:20:57 +0700 Subject: [PATCH 1/7] Add Finder::nth() method to find the matched item at a specific occurrence, or matched items at multiple occurrences --- src/Finder.php | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/Finder.php b/src/Finder.php index dee410b..3de31dc 100644 --- a/src/Finder.php +++ b/src/Finder.php @@ -11,8 +11,11 @@ use Traversable; use Webmozart\Assert\Assert; +use function count; use function current; use function end; +use function in_array; +use function is_int; use function is_numeric; use function iterator_to_array; use function key; @@ -218,4 +221,62 @@ public static function partition( return [$matching, $notMatching]; } + + /** + * Find the matched item at a specific occurrence, or matched items at multiple occurrences. + * + * @param array|Traversable $data + * @param callable(mixed $datum, int|string|null $key): bool $filter + * @param int|array $n The occurrence(s) to find + */ + public static function nth(iterable $data, callable $filter, int|array $n, bool $returnKey = false): mixed + { + // filter must be a callable with bool return type + Filter::boolean($filter); + + $singleMode = is_int($n); + + if ($singleMode) { + Assert::positiveInteger($n); + $positions = [$n]; + } else { + Assert::notEmpty($n); + Assert::uniqueValues($n); + Assert::allPositiveInteger($n); + + $positions = $n; + } + + $currentMatchIndex = 0; + $collectedCount = 0; + $totalExpected = count($positions); + $result = []; + + foreach ($data as $key => $datum) { + if (! $filter($datum, $key)) { + continue; + } + + ++$currentMatchIndex; + + if (! in_array($currentMatchIndex, $positions, true)) { + continue; + } + + $value = $returnKey ? $key : $datum; + + if ($singleMode) { + return $value; + } + + $result[] = $value; + ++$collectedCount; + + if ($collectedCount === $totalExpected) { + break; + } + } + + return $singleMode ? null : $result; + } } From c48f21cd412152b6f4086192144adcc7e38cf6b8 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 20 Apr 2026 11:27:23 +0700 Subject: [PATCH 2/7] update description --- src/Finder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Finder.php b/src/Finder.php index 3de31dc..f183c35 100644 --- a/src/Finder.php +++ b/src/Finder.php @@ -223,7 +223,7 @@ public static function partition( } /** - * Find the matched item at a specific occurrence, or matched items at multiple occurrences. + * Find the 1st, 2nd, 3rd, and so on matching item, or multiple matching items at specific positions. * * @param array|Traversable $data * @param callable(mixed $datum, int|string|null $key): bool $filter From 296883f53a12d81c71ce82dc7ed02ae62c821d12 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 20 Apr 2026 11:28:32 +0700 Subject: [PATCH 3/7] update param description --- src/Finder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Finder.php b/src/Finder.php index f183c35..37677f7 100644 --- a/src/Finder.php +++ b/src/Finder.php @@ -227,7 +227,7 @@ public static function partition( * * @param array|Traversable $data * @param callable(mixed $datum, int|string|null $key): bool $filter - * @param int|array $n The occurrence(s) to find + * @param int|array $n The match position(s) to find */ public static function nth(iterable $data, callable $filter, int|array $n, bool $returnKey = false): mixed { From 286e466b2d2c22293397df35bf76ae2113ef5692 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 20 Apr 2026 13:19:31 +0700 Subject: [PATCH 4/7] add unit test --- tests/FinderTest.php | 58 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/FinderTest.php b/tests/FinderTest.php index f348565..44af9bb 100644 --- a/tests/FinderTest.php +++ b/tests/FinderTest.php @@ -469,4 +469,62 @@ public function testPartitionPreservesOriginalData(): void $this->assertSame([1], $notMatching); $this->assertSame($copy, $data); } + + /** + * @param mixed $expected + * @param int[] $data + */ + #[DataProvider('nthDataProvider')] + public function testNth(iterable $data, callable $filter, int|array $n, int|array|null $expected, bool $returnKey = false): void + { + $this->assertSame( + $expected, + Finder::nth($data, $filter, $n, $returnKey) + ); + } + + /** + * @return Iterator + */ + public static function nthDataProvider(): Iterator + { + yield 'single nth match' => [ + [10, 20, 30, 40, 50], + static fn($datum): bool => $datum > 15, + 2, + 30, + ]; + yield 'single nth no match' => [ + [10, 20, 30], + static fn($datum): bool => $datum > 15, + 5, + null, + ]; + yield 'multiple nth matches' => [ + [10, 20, 30, 40, 50], + static fn($datum): bool => $datum > 15, + [1, 3], + [20, 40], + ]; + yield 'multiple nth no matches' => [ + [10, 20, 30], + static fn($datum): bool => $datum > 15, + [5, 6], + [], + ]; + yield 'single nth match returnKey' => [ + [10, 20, 30, 40, 50], + static fn($datum): bool => $datum > 15, + 2, + 2, + true, + ]; + yield 'multiple nth matches returnKey' => [ + [10, 20, 30, 40, 50], + static fn($datum): bool => $datum > 15, + [1, 3], + [1, 3], + true, + ]; + } } From 3f722a1f8addc06beea34b8ec2a27a1c48eb2a85 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 20 Apr 2026 13:21:33 +0700 Subject: [PATCH 5/7] cs fix --- tests/FinderTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/FinderTest.php b/tests/FinderTest.php index 44af9bb..68efd48 100644 --- a/tests/FinderTest.php +++ b/tests/FinderTest.php @@ -475,8 +475,13 @@ public function testPartitionPreservesOriginalData(): void * @param int[] $data */ #[DataProvider('nthDataProvider')] - public function testNth(iterable $data, callable $filter, int|array $n, int|array|null $expected, bool $returnKey = false): void - { + public function testNth( + iterable $data, + callable $filter, + int|array $n, + int|array|null $expected, + bool $returnKey = false + ): void { $this->assertSame( $expected, Finder::nth($data, $filter, $n, $returnKey) From f1212ec7ab59cf2380d788b29842a11f4e8d16f4 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 20 Apr 2026 13:27:04 +0700 Subject: [PATCH 6/7] update readme --- README.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 098b46a..c149dc1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Features - [x] Verify exact times: [`once()`](#1-onlyonce), [`twice()`](#2-onlytwice), [`times()`](#3-onlytimes) - [x] Verify in interval range: [`isInclusiveOf()`](#1-intervalisinclusiveof), [`isExclusiveOf()`](#2-intervalisexclusiveof) - [x] Verify all or none match: [`match()`](#1-allmatch), [`none()`](#2-allnone) -- [x] Search data: [`first()`](#1-finderfirst), [`last()`](#2-finderlast), [`rows()`](#3-finderrows), [`partition()`](#4-finderpartition) +- [x] Search data: [`first()`](#1-finderfirst), [`last()`](#2-finderlast), [`nth()`](#3-findernth), [`rows()`](#4-finderrows), [`partition()`](#5-finderpartition) - [x] Collect data with [filter and transform](#g-collector) Installation @@ -503,7 +503,37 @@ var_dump(Finder::last( )); // null ``` -#### 3. `Finder::rows()` + +#### 3. `Finder::nth()` + +It returns the nth (or multiple nth) filtered data found. You can specify a single position (1-based) or an array of positions. If no match is found, returns `null` (single) or an empty array (multiple). Pass `true` to the 4th argument to return the key(s) instead of the value(s). + +```php +use ArrayLookup\Finder; + +$data = [10, 20, 30, 40, 50]; +$filter = static fn($datum): bool => $datum > 15; + +// Get the 2nd matching value (should be 30) +var_dump(Finder::nth($data, $filter, 2)); // 30 + +// Get the 2nd matching key (should be 2) +var_dump(Finder::nth($data, $filter, 2, returnKey: true)); // 2 + +// Get the 1st and 3rd matching values (should be [20, 40]) +var_dump(Finder::nth($data, $filter, [1, 3])); // [20, 40] + +// Get the 1st and 3rd matching keys (should be [1, 3]) +var_dump(Finder::nth($data, $filter, [1, 3], returnKey: true)); // [1, 3] + +// No match (single) +var_dump(Finder::nth($data, $filter, 5)); // null + +// No match (multiple) +var_dump(Finder::nth($data, $filter, [5, 6])); // [] +``` + +#### 4. `Finder::rows()` It get rows data filtered found. From 8dc1cfc09aafa9bfb0d5226c424c1ccb6ec56da9 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 20 Apr 2026 13:32:00 +0700 Subject: [PATCH 7/7] update readme --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c149dc1..52b5716 100644 --- a/README.md +++ b/README.md @@ -503,10 +503,9 @@ var_dump(Finder::last( )); // null ``` - #### 3. `Finder::nth()` -It returns the nth (or multiple nth) filtered data found. You can specify a single position (1-based) or an array of positions. If no match is found, returns `null` (single) or an empty array (multiple). Pass `true` to the 4th argument to return the key(s) instead of the value(s). +It returns the 1st, 2nd, 3rd, and so on matching item, or multiple matching items at specific positions. The position is 1-based among matched results, not the original array index. Pass a single position to get one matching item, or an array of positions to get multiple matching items. If no match is found, it returns `null` for a single position or an empty array for multiple positions. Pass `true` to the 4th argument to return the key(s) instead of the value(s). ```php use ArrayLookup\Finder; @@ -518,13 +517,13 @@ $filter = static fn($datum): bool => $datum > 15; var_dump(Finder::nth($data, $filter, 2)); // 30 // Get the 2nd matching key (should be 2) -var_dump(Finder::nth($data, $filter, 2, returnKey: true)); // 2 +var_dump(Finder::nth($data, $filter, 2, true)); // 2 // Get the 1st and 3rd matching values (should be [20, 40]) var_dump(Finder::nth($data, $filter, [1, 3])); // [20, 40] // Get the 1st and 3rd matching keys (should be [1, 3]) -var_dump(Finder::nth($data, $filter, [1, 3], returnKey: true)); // [1, 3] +var_dump(Finder::nth($data, $filter, [1, 3], true)); // [1, 3] // No match (single) var_dump(Finder::nth($data, $filter, 5)); // null