├── LICENSE ├── README.md ├── composer.json └── src ├── Assert └── Filter.php ├── AtLeast.php ├── Collector.php ├── Finder.php └── Only.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Abdul Malik Ikhsan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ArrayLookup 2 | =============== 3 | 4 | [![Latest Version](https://img.shields.io/github/release/samsonasik/ArrayLookup.svg?style=flat-square)](https://github.com/samsonasik/ArrayLookup/releases) 5 | ![ci build](https://github.com/samsonasik/ArrayLookup/workflows/ci%20build/badge.svg) 6 | [![Code Coverage](https://codecov.io/gh/samsonasik/ArrayLookup/branch/main/graph/badge.svg)](https://codecov.io/gh/samsonasik/ArrayLookup) 7 | [![PHPStan](https://img.shields.io/badge/style-level%20max-brightgreen.svg?style=flat-square&label=phpstan)](https://github.com/phpstan/phpstan) 8 | [![Downloads](https://poser.pugx.org/samsonasik/array-lookup/downloads)](https://packagist.org/packages/samsonasik/array-lookup) 9 | 10 | Introduction 11 | ------------ 12 | 13 | ArrayLookup is a fast lookup library that help you verify and search `array` and `Traversable` data. 14 | 15 | Features 16 | -------- 17 | 18 | - [x] Verify at least times: `once()`, `twice()`, `times()` 19 | - [x] Verify exact times: `once()`, `twice()`, `times()` 20 | - [x] Search data: `first()`, `last()`, `rows()` 21 | - [x] Collect data with filter and transform 22 | 23 | Installation 24 | ------------ 25 | 26 | **Require this library uses [composer](https://getcomposer.org/).** 27 | 28 | ```sh 29 | composer require samsonasik/array-lookup 30 | ``` 31 | 32 | Usage 33 | ----- 34 | 35 | **A. AtLeast** 36 | --------------- 37 | 38 | #### 1. `AtLeast::once()` 39 | 40 | It verify that data has filtered found item at least once. 41 | 42 | ```php 43 | use ArrayLookup\AtLeast; 44 | 45 | $data = [1, 2, 3]; 46 | $filter = static fn($datum): bool => $datum === 1; 47 | 48 | var_dump(AtLeast::once($data, $filter)) // true 49 | 50 | $data = [1, 2, 3]; 51 | $filter = static fn($datum): bool => $datum === 4; 52 | 53 | var_dump(AtLeast::once($data, $filter)) // false 54 | 55 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 56 | 57 | $data = [1, 2, 3]; 58 | $filter = static fn($datum, $key): bool => $datum === 1 && $key >= 0; 59 | 60 | var_dump(AtLeast::once($data, $filter)) // true 61 | 62 | $data = [1, 2, 3]; 63 | $filter = static fn($datum, $key): bool => $datum === 4 && $key >= 0; 64 | 65 | var_dump(AtLeast::once($data, $filter)) // false 66 | ``` 67 | 68 | #### 2. `AtLeast::twice()` 69 | 70 | It verify that data has filtered found items at least twice. 71 | 72 | ```php 73 | use ArrayLookup\AtLeast; 74 | 75 | $data = [1, "1", 3]; 76 | $filter = static fn($datum): bool => $datum == 1; 77 | 78 | var_dump(AtLeast::twice($data, $filter)) // true 79 | 80 | $data = [1, "1", 3]; 81 | $filter = static fn($datum): bool => $datum === 1; 82 | 83 | var_dump(AtLeast::twice($data, $filter)) // false 84 | 85 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 86 | 87 | $data = [1, "1", 3]; 88 | $filter = static fn($datum, $key): bool => $datum == 1 && $key >= 0; 89 | 90 | var_dump(AtLeast::twice($data, $filter)) // true 91 | 92 | $data = [1, "1", 3]; 93 | $filter = static fn($datum, $key): bool => $datum === 1 && $key >= 0; 94 | 95 | var_dump(AtLeast::twice($data, $filter)) // false 96 | ``` 97 | 98 | #### 3. `AtLeast::times()` 99 | 100 | It verify that data has filtered found items at least times passed in 3rd arg. 101 | 102 | ```php 103 | use ArrayLookup\AtLeast; 104 | 105 | $data = [false, null, 0]; 106 | $filter = static fn($datum): bool => ! $datum; 107 | $times = 3; 108 | 109 | var_dump(AtLeast::times($data, $filter, $times)) // true 110 | 111 | $data = [1, null, 0]; 112 | $filter = static fn($datum): bool => ! $datum; 113 | $times = 3; 114 | 115 | var_dump(AtLeast::times($data, $filter, $times)) // false 116 | 117 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 118 | 119 | $data = [false, null, 0]; 120 | $filter = static fn($datum, $key): bool => ! $datum && $key >= 0; 121 | $times = 3; 122 | 123 | var_dump(AtLeast::times($data, $filter, $times)) // true 124 | 125 | $data = [1, null, 0]; 126 | $filter = static fn($datum, $key): bool => ! $datum && $key >= 0; 127 | $times = 3; 128 | 129 | var_dump(AtLeast::times($data, $filter, $times)) // false 130 | ``` 131 | 132 | **B. Only** 133 | --------------- 134 | 135 | #### 1. `Only::once()` 136 | 137 | It verify that data has filtered found item exactly found only once. 138 | 139 | ```php 140 | use ArrayLookup\Only; 141 | 142 | $data = [1, 2, 3]; 143 | $filter = static fn($datum): bool => $datum === 1; 144 | 145 | var_dump(Only::once($data, $filter)) // true 146 | 147 | 148 | $data = [1, "1", 3] 149 | $filter = static fn($datum): bool => $datum == 1; 150 | 151 | var_dump(Only::once($data, $filter)) // false 152 | 153 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 154 | 155 | $data = [1, 2, 3]; 156 | $filter = static fn($datum, $key): bool => $datum === 1 && $key >= 0; 157 | 158 | var_dump(Only::once($data, $filter)) // true 159 | 160 | 161 | $data = [1, "1", 3] 162 | $filter = static fn($datum, $key): bool => $datum == 1 && $key >= 0; 163 | 164 | var_dump(Only::once($data, $filter)) // false 165 | ``` 166 | 167 | #### 2. `Only::twice()` 168 | 169 | It verify that data has filtered found items exactly found only twice. 170 | 171 | ```php 172 | use ArrayLookup\Only; 173 | 174 | $data = [1, "1", 3]; 175 | $filter = static fn($datum): bool => $datum == 1; 176 | 177 | var_dump(Only::twice($data, $filter)) // true 178 | 179 | $data = [true, 1, new stdClass()]; 180 | $filter = static fn($datum): bool => (bool) $datum; 181 | 182 | var_dump(Only::twice($data, $filter)) // false 183 | 184 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 185 | 186 | $data = [1, "1", 3]; 187 | $filter = static fn($datum, $key): bool => $datum == 1 && $key >= 0; 188 | 189 | var_dump(Only::twice($data, $filter)) // true 190 | 191 | $data = [true, 1, new stdClass()]; 192 | $filter = static fn($datum, $key): bool => (bool) $datum && $key >= 0; 193 | 194 | var_dump(Only::twice($data, $filter)) // false 195 | ``` 196 | 197 | #### 3. `Only::times()` 198 | 199 | It verify that data has filtered found items exactly found only same with times passed in 3rd arg. 200 | 201 | ```php 202 | use ArrayLookup\Only; 203 | 204 | $data = [false, null, 1]; 205 | $filter = static fn($datum): bool => ! $datum; 206 | $times = 2; 207 | 208 | var_dump(Only::times($data, $filter, $times)) // true 209 | 210 | 211 | $data = [false, null, 0]; 212 | $filter = static fn($datum): bool => ! $datum; 213 | $times = 2; 214 | 215 | var_dump(Only::times($data, $filter, $times)) // false 216 | 217 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 218 | 219 | $data = [false, null, 1]; 220 | $filter = static fn($datum, $key): bool => ! $datum && $key >= 0; 221 | $times = 2; 222 | 223 | var_dump(Only::times($data, $filter, $times)) // true 224 | 225 | 226 | $data = [false, null, 0]; 227 | $filter = static fn($datum, $key): bool => ! $datum && $key >= 0; 228 | $times = 2; 229 | 230 | var_dump(Only::times($data, $filter, $times)) // false 231 | ``` 232 | 233 | **C. Finder** 234 | --------------- 235 | 236 | #### 1. `Finder::first()` 237 | 238 | It search first data filtered found. 239 | 240 | ```php 241 | use ArrayLookup\Finder; 242 | 243 | $data = [1, 2, 3]; 244 | $filter = static fn($datum): bool => $datum === 1; 245 | 246 | var_dump(Finder::first($data, $filter)) // 1 247 | 248 | $filter = static fn($datum): bool => $datum == 1000; 249 | var_dump(Finder::first($data, $filter)) // null 250 | 251 | // RETURN the Array key, pass true to 3rd arg 252 | 253 | $filter = static fn($datum): bool => $datum === 1; 254 | 255 | var_dump(Finder::first($data, $filter, true)) // 0 256 | 257 | $filter = static fn($datum): bool => $datum == 1000; 258 | var_dump(Finder::first($data, $filter, true)) // null 259 | 260 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 261 | 262 | $filter = static fn($datum, $key): bool => $datum === 1 && $key >= 0; 263 | 264 | var_dump(Finder::first($data, $filter)) // 1 265 | 266 | $filter = static fn($datum, $key): bool => $datum == 1000 && $key >= 0; 267 | var_dump(Finder::first($data, $filter)) // null 268 | ``` 269 | 270 | #### 2. `Finder::last()` 271 | 272 | It search last data filtered found. 273 | 274 | ```php 275 | use ArrayLookup\Finder; 276 | 277 | $data = [6, 7, 8, 9]; 278 | var_dump(Finder::last( 279 | $data, 280 | static fn ($datum): bool => $datum > 5 281 | )); // 9 282 | 283 | var_dump(Finder::last( 284 | $data, 285 | static fn ($datum): bool => $datum < 5 286 | )); // null 287 | 288 | // RETURN the Array key, pass true to 3rd arg 289 | 290 | // ... with PRESERVE original key 291 | var_dump(Finder::last( 292 | $data, 293 | static fn ($datum): bool => $datum > 5, 294 | true 295 | )); // 3 296 | 297 | // ... with RESORT key, first key is last record 298 | var_dump(Finder::last( 299 | $data, 300 | static fn ($datum): bool => $datum > 5, 301 | true, 302 | false 303 | )); // 0 304 | 305 | var_dump(Finder::last( 306 | $data, 307 | static fn ($datum): bool => $datum < 5, 308 | true 309 | )); // null 310 | 311 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 312 | 313 | var_dump(Finder::last( 314 | $data, 315 | static fn ($datum, $key): bool => $datum > 5 && $key >= 0 316 | )); // 9 317 | 318 | var_dump(Finder::last( 319 | $data, 320 | static fn ($datum, $key): bool => $datum < 5 && $key >= 0 321 | )); // null 322 | ``` 323 | 324 | #### 3. `Finder::rows()` 325 | 326 | It get rows data filtered found. 327 | 328 | ```php 329 | use ArrayLookup\Finder; 330 | 331 | $data = [6, 7, 8, 9]; 332 | var_dump(Finder::rows( 333 | $data, 334 | static fn($datum): bool => $datum > 6 335 | )); // [7, 8, 9] 336 | 337 | var_dump(Finder::rows( 338 | $data, 339 | static fn ($datum): bool => $datum < 5 340 | )); // [] 341 | 342 | // ... with PRESERVE original key 343 | var_dump(Finder::rows( 344 | $data, 345 | static fn ($datum): bool => $datum > 6, 346 | true 347 | )); // [1 => 7, 2 => 8, 3 => 9] 348 | 349 | var_dump(Finder::rows( 350 | $data, 351 | static fn ($datum): bool => $datum < 5, 352 | true 353 | )); // [] 354 | 355 | // WITH key array included, pass $key variable as 2nd arg on filter to be used in filter 356 | var_dump(Finder::rows( 357 | $data, 358 | static fn($datum, $key): bool => $datum > 6 && $key > 1 359 | )); // [8, 9] 360 | 361 | 362 | // WITH gather only limited found data 363 | $data = [1, 2]; 364 | $filter = static fn($datum): bool => $datum >= 0; 365 | $limit = 1; 366 | 367 | var_dump( 368 | Finder::rows($data, $filter, limit: $limit) 369 | ); // [1] 370 | ``` 371 | 372 | **D. Collector** 373 | --------------- 374 | 375 | It collect filtered data, with new transformed each data found: 376 | 377 | **Before** 378 | 379 | ```php 380 | $newArray = []; 381 | 382 | foreach ($data as $datum) { 383 | if (is_string($datum)) { 384 | $newArray[] = trim($datum); 385 | } 386 | } 387 | ``` 388 | 389 | **After** 390 | 391 | ```php 392 | use ArrayLookup\Collector; 393 | 394 | $when = fn ($datum): bool => is_string($datum); 395 | $limit = 2; 396 | $transform = fn ($datum): string => trim($datum); 397 | 398 | $newArray = Collector::setUp($data) 399 | ->when($when) // optional, can just transform without filtering 400 | ->withLimit(2) // optional to only collect some data provided by limit config 401 | ->withTransform($transform) 402 | ->getResults(); 403 | ``` 404 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samsonasik/array-lookup", 3 | "type": "library", 4 | "description": "A fast lookup library that help you verify and search array and Traversable data", 5 | "keywords": [ 6 | "array", 7 | "traversable", 8 | "iterable", 9 | "search", 10 | "filter", 11 | "fast", 12 | "lookup" 13 | ], 14 | "homepage": "https://github.com/samsonasik/ArrayLookup", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Abdul Malik Ikhsan", 19 | "email": "samsonasik@gmail.com", 20 | "homepage": "http://samsonasik.wordpress.com", 21 | "role": "Developer" 22 | } 23 | ], 24 | "require": { 25 | "php": "^8.2", 26 | "webmozart/assert": "^1.11" 27 | }, 28 | "require-dev": { 29 | "laminas/laminas-coding-standard": "^3.0", 30 | "phpstan/phpstan": "^2.0", 31 | "phpstan/phpstan-webmozart-assert": "^2.0", 32 | "phpunit/phpunit": "^11.0", 33 | "rector/rector": "dev-main" 34 | }, 35 | "config": { 36 | "sort-packages": true, 37 | "allow-plugins": { 38 | "dealerdirect/phpcodesniffer-composer-installer": true 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "ArrayLookup\\": "src/" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "ArrayLookup\\Tests\\": "tests/" 49 | } 50 | }, 51 | "minimum-stability": "dev", 52 | "prefer-stable": true, 53 | "scripts": { 54 | "cs-check": "phpcs", 55 | "cs-fix": "phpcbf", 56 | "phpstan": "phpstan analyse src/ --level=max -c phpstan.neon", 57 | "rector": "rector process --dry-run", 58 | "test": "phpunit" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Assert/Filter.php: -------------------------------------------------------------------------------- 1 | new ReflectionFunction($filter), 33 | is_object($filter) => new ReflectionMethod($filter, '__invoke'), 34 | default => throw new InvalidArgumentException( 35 | sprintf('Expected Closure or invokable object on callable filter, %s given', gettype($filter)) 36 | ), 37 | }; 38 | 39 | $returnType = $reflection->getReturnType(); 40 | 41 | if ($returnType instanceof ReflectionUnionType || $returnType instanceof ReflectionIntersectionType) { 42 | $separator = $returnType instanceof ReflectionUnionType ? '|' : '&'; 43 | $types = $returnType->getTypes(); 44 | 45 | Assert::allIsInstanceOf($types, ReflectionNamedType::class); 46 | 47 | throw new InvalidArgumentException( 48 | sprintf( 49 | self::MESSAGE, 50 | implode($separator, array_map( 51 | static fn (ReflectionNamedType $reflectionNamedType): string => $reflectionNamedType->getName(), 52 | $types 53 | )) 54 | ) 55 | ); 56 | } 57 | 58 | if (! $returnType instanceof ReflectionNamedType) { 59 | throw new InvalidArgumentException(sprintf(self::MESSAGE, 'mixed')); 60 | } 61 | 62 | $returnTypeName = $returnType->getName(); 63 | if ($returnTypeName !== 'bool') { 64 | throw new InvalidArgumentException(sprintf( 65 | self::MESSAGE, 66 | $returnTypeName 67 | )); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/AtLeast.php: -------------------------------------------------------------------------------- 1 | |Traversable $data 15 | * @param callable(mixed $datum, int|string|null $key): bool $filter 16 | */ 17 | public static function once(iterable $data, callable $filter): bool 18 | { 19 | return self::atLeastFoundTimes($data, $filter, 1); 20 | } 21 | 22 | /** 23 | * @param array|Traversable $data 24 | * @param callable(mixed $datum, int|string|null $key): bool $filter 25 | */ 26 | public static function twice(iterable $data, callable $filter): bool 27 | { 28 | return self::atLeastFoundTimes($data, $filter, 2); 29 | } 30 | 31 | /** 32 | * @param array|Traversable $data 33 | * @param callable(mixed $datum, int|string|null $key): bool $filter 34 | */ 35 | public static function times(iterable $data, callable $filter, int $count): bool 36 | { 37 | return self::atLeastFoundTimes($data, $filter, $count); 38 | } 39 | 40 | /** 41 | * @param array|Traversable $data 42 | * @param callable(mixed $datum, int|string|null $key): bool $filter 43 | */ 44 | private static function atLeastFoundTimes( 45 | iterable $data, 46 | callable $filter, 47 | int $maxCount 48 | ): bool { 49 | // usage must be higher than 0 50 | Assert::greaterThan($maxCount, 0); 51 | // filter must be a callable with bool return type 52 | Filter::boolean($filter); 53 | 54 | $totalFound = 0; 55 | foreach ($data as $key => $datum) { 56 | $isFound = $filter($datum, $key); 57 | 58 | if (! $isFound) { 59 | continue; 60 | } 61 | 62 | ++$totalFound; 63 | 64 | if ($totalFound === $maxCount) { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Collector.php: -------------------------------------------------------------------------------- 1 | |Traversable */ 16 | private iterable $data = []; 17 | 18 | /** @var null|callable(mixed $datum, int|string|null $key): bool */ 19 | private $when; 20 | 21 | /** @var null|callable(mixed $datum, int|string|null $key): mixed */ 22 | private $transform; 23 | 24 | private ?int $limit = null; 25 | 26 | /** 27 | * @param array|Traversable $data 28 | */ 29 | public static function setUp(iterable $data): self 30 | { 31 | $self = new self(); 32 | $self->data = $data; 33 | 34 | return $self; 35 | } 36 | 37 | /** 38 | * @param callable(mixed $datum, int|string|null $key): bool $filter 39 | */ 40 | public function when(callable $filter): self 41 | { 42 | $this->when = $filter; 43 | return $this; 44 | } 45 | 46 | public function withLimit(int $limit): self 47 | { 48 | Assert::positiveInteger($limit); 49 | $this->limit = $limit; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * @param callable(mixed $datum, int|string|null $key): mixed $transform 56 | */ 57 | public function withTransform(callable $transform): self 58 | { 59 | $this->transform = $transform; 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return mixed[] 65 | */ 66 | public function getResults(): array 67 | { 68 | // ensure transform property is set early ->withTransform() method 69 | Assert::isCallable($this->transform); 70 | 71 | $count = 0; 72 | $collectedData = []; 73 | 74 | if (is_callable($this->when)) { 75 | // filter must be a callable with bool return type 76 | Filter::boolean($this->when); 77 | } 78 | 79 | foreach ($this->data as $key => $datum) { 80 | if ($this->when !== null) { 81 | $isFound = ($this->when)($datum, $key); 82 | 83 | if (! $isFound) { 84 | continue; 85 | } 86 | } 87 | 88 | $collectedData[] = ($this->transform)($datum, $key); 89 | 90 | ++$count; 91 | 92 | if ($this->limit === $count) { 93 | break; 94 | } 95 | } 96 | 97 | return $collectedData; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Finder.php: -------------------------------------------------------------------------------- 1 | |Traversable $data 24 | * @param callable(mixed $datum, int|string|null $key): bool $filter 25 | */ 26 | public static function first(iterable $data, callable $filter, bool $returnKey = false): mixed 27 | { 28 | // filter must be a callable with bool return type 29 | Filter::boolean($filter); 30 | 31 | foreach ($data as $key => $datum) { 32 | $isFound = $filter($datum, $key); 33 | 34 | if (! $isFound) { 35 | continue; 36 | } 37 | 38 | return $returnKey ? $key : $datum; 39 | } 40 | 41 | return null; 42 | } 43 | 44 | /** 45 | * @param Traversable $traversable 46 | * @return array 47 | */ 48 | private static function resolveArrayFromTraversable(Traversable $traversable): array 49 | { 50 | if ($traversable instanceof ArrayIterator || $traversable instanceof ArrayObject) { 51 | return $traversable->getArrayCopy(); 52 | } 53 | 54 | return iterator_to_array($traversable); 55 | } 56 | 57 | /** 58 | * @param array|Traversable $data 59 | * @param callable(mixed $datum, int|string|null $key): bool $filter 60 | */ 61 | public static function last( 62 | iterable $data, 63 | callable $filter, 64 | bool $returnKey = false, 65 | bool $preserveKey = true 66 | ): mixed { 67 | // convert to array when data is Traversable instance 68 | if ($data instanceof Traversable) { 69 | $data = self::resolveArrayFromTraversable($data); 70 | } 71 | 72 | // ensure data is array for end(), key(), current(), prev() usage 73 | Assert::isArray($data); 74 | 75 | // filter must be a callable with bool return type 76 | Filter::boolean($filter); 77 | 78 | // Use end(), key(), current(), prev() usage instead of array_reverse() 79 | // to avoid immediately got "Out of memory" on many data 80 | // see https://3v4l.org/IHo2H vs https://3v4l.org/Wqejc 81 | 82 | // go to end of array 83 | end($data); 84 | 85 | // grab current key 86 | $key = key($data); 87 | $resortkey = -1; 88 | 89 | // key = null means no longer current data 90 | while ($key !== null) { 91 | if (! $preserveKey && $returnKey) { 92 | ++$resortkey; 93 | } 94 | 95 | $current = current($data); 96 | $isFound = $filter($current, $key); 97 | 98 | if (! $isFound) { 99 | // go to previous row 100 | prev($data); 101 | 102 | // re-set key variable with new key value of previous row 103 | $key = key($data); 104 | 105 | continue; 106 | } 107 | 108 | if (! $returnKey) { 109 | return $current; 110 | } 111 | 112 | if ($preserveKey) { 113 | return $key; 114 | } 115 | 116 | return $resortkey; 117 | } 118 | 119 | return null; 120 | } 121 | 122 | /** 123 | * @param array|Traversable $data 124 | * @param callable(mixed $datum, int|string|null $key): bool $filter 125 | * @return mixed[] 126 | */ 127 | public static function rows( 128 | iterable $data, 129 | callable $filter, 130 | bool $preserveKey = false, 131 | ?int $limit = null 132 | ): array { 133 | $rows = []; 134 | $newKey = 0; 135 | $totalFound = 0; 136 | 137 | // filter must be a callable with bool return type 138 | Filter::boolean($filter); 139 | 140 | foreach ($data as $key => $datum) { 141 | $isFound = $filter($datum, $key); 142 | 143 | if (! $isFound) { 144 | continue; 145 | } 146 | 147 | if ($preserveKey || ! is_numeric($key)) { 148 | $rowKey = $key; 149 | } else { 150 | $rowKey = $newKey; 151 | ++$newKey; 152 | } 153 | 154 | $rows[$rowKey] = $datum; 155 | 156 | if ($limit === null) { 157 | continue; 158 | } 159 | 160 | ++$totalFound; 161 | if ($totalFound === $limit) { 162 | break; 163 | } 164 | } 165 | 166 | return $rows; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Only.php: -------------------------------------------------------------------------------- 1 | |Traversable $data 15 | * @param callable(mixed $datum, int|string|null $key): bool $filter 16 | */ 17 | public static function once(iterable $data, callable $filter): bool 18 | { 19 | return self::onlyFoundTimes($data, $filter, 1); 20 | } 21 | 22 | /** 23 | * @param array|Traversable $data 24 | * @param callable(mixed $datum, int|string|null $key): bool $filter 25 | */ 26 | public static function twice(iterable $data, callable $filter): bool 27 | { 28 | return self::onlyFoundTimes($data, $filter, 2); 29 | } 30 | 31 | /** 32 | * @param array|Traversable $data 33 | * @param callable(mixed $datum, int|string|null $key): bool $filter 34 | */ 35 | public static function times(iterable $data, callable $filter, int $count): bool 36 | { 37 | return self::onlyFoundTimes($data, $filter, $count); 38 | } 39 | 40 | /** 41 | * @param array|Traversable $data 42 | * @param callable(mixed $datum, int|string|null $key): bool $filter 43 | */ 44 | private static function onlyFoundTimes( 45 | iterable $data, 46 | callable $filter, 47 | int $maxCount 48 | ): bool { 49 | // usage must be higher than 0 50 | Assert::greaterThan($maxCount, 0); 51 | // filter must be a callable with bool return type 52 | Filter::boolean($filter); 53 | 54 | $totalFound = 0; 55 | foreach ($data as $key => $datum) { 56 | $isFound = $filter($datum, $key); 57 | 58 | if (! $isFound) { 59 | continue; 60 | } 61 | 62 | // total found already passed maxCount but found new one? stop 63 | if ($totalFound === $maxCount) { 64 | return false; 65 | } 66 | 67 | ++$totalFound; 68 | } 69 | 70 | return $totalFound === $maxCount; 71 | } 72 | } 73 | --------------------------------------------------------------------------------