Skip to content
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
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -503,7 +503,36 @@ var_dump(Finder::last(
)); // null
```

#### 3. `Finder::rows()`
#### 3. `Finder::nth()`

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;

$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, 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], 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.

Expand Down
61 changes: 61 additions & 0 deletions src/Finder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -218,4 +221,62 @@ public static function partition(

return [$matching, $notMatching];
}

/**
* Find the 1st, 2nd, 3rd, and so on matching item, or multiple matching items at specific positions.
*
* @param array<int|string, mixed>|Traversable<int|string, mixed> $data
* @param callable(mixed $datum, int|string|null $key): bool $filter
* @param int|array<int, int> $n The match position(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;
}
}
63 changes: 63 additions & 0 deletions tests/FinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,4 +469,67 @@ 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<mixed>
*/
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,
];
}
}
Loading