├── .gitignore ├── phpunit.xml ├── README.md ├── example.php ├── composer.json ├── tests └── Itertools │ └── ItertoolsTest.php └── src └── Itertools └── Itertools.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .phpunit.cache/ 3 | composer.lock 4 | .idea/ 5 | .phpunit.result.cache -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # itertools 2 | 3 | "Fair" (based on PHP 5.5 generators) implementation of the [Python itertools](https://docs.python.org/3/library/itertools.html) module. 4 | 5 | All functions are implemented; PHP 8.1 or higher is required to run. 6 | 7 | Usage example: 8 | 9 | ```php 10 | require_once 'src/Itertools/Itertools.php'; 11 | use Itertools\Itertools as it; 12 | 13 | foreach (it::islice(it::cycle('ABC'), 10) as $element) { 14 | echo $element; 15 | } 16 | ``` 17 | Compared to the itertools package, this implementation includes standard Python functions such as `iter`, 18 | `enumerate` and `range`. 19 | -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | assertEquals([[0, 1], [1, 2], [2, 3]], $result); 16 | } 17 | 18 | public function testRange(): void 19 | { 20 | $result = iterator_to_array(it::range(2, 10, 2)); 21 | $this->assertEquals([2, 4, 6, 8], $result); 22 | } 23 | 24 | public function testChain(): void 25 | { 26 | $result = iterator_to_array(it::chain([1, 2], [3, 4])); 27 | $this->assertEquals([1, 2, 3, 4], $result); 28 | } 29 | 30 | public function testCombinations(): void 31 | { 32 | $result = iterator_to_array(it::combinations([1, 2, 3], 2)); 33 | $this->assertEquals([[1, 2], [1, 3], [2, 3]], $result); 34 | } 35 | 36 | public function testCombinationsWithReplacement(): void 37 | { 38 | $result = iterator_to_array(it::combinations_with_replacement([1, 2], 2)); 39 | $this->assertEquals([[1, 1], [1, 2], [2, 2]], $result); 40 | } 41 | 42 | public function testCompress(): void 43 | { 44 | $result = iterator_to_array(it::compress([1, 2, 3], [1, 0, 1])); 45 | $this->assertEquals([1, 3], $result); 46 | } 47 | 48 | public function testIslice(): void 49 | { 50 | $result = iterator_to_array(it::islice([1, 2, 3, 4, 5], 1, 4, 2)); 51 | $this->assertEquals([2, 4], $result); 52 | } 53 | 54 | public function testCount(): void 55 | { 56 | $result = it::count(1, 2); 57 | $this->assertEquals([1, 3, 5, 7, 9], iterator_to_array(it::islice($result, 5))); 58 | } 59 | 60 | public function testCycle(): void 61 | { 62 | $result = it::cycle([1, 2]); 63 | $this->assertEquals([1, 2, 1, 2, 1], iterator_to_array(it::islice($result, 5))); 64 | } 65 | 66 | public function testDropwhile(): void 67 | { 68 | $result = iterator_to_array(it::dropwhile(fn($x) => $x < 3, [1, 2, 3, 4])); 69 | $this->assertEquals([3, 4], $result); 70 | } 71 | 72 | public function testGroupby(): void 73 | { 74 | $result = iterator_to_array(it::groupby('AAAABBBCCD')); 75 | 76 | $this->assertEquals([ 77 | ['A', ['A', 'A', 'A', 'A']], 78 | ['B', ['B', 'B', 'B']], 79 | ['C', ['C', 'C']], 80 | ['D', ['D']], 81 | ], $result); 82 | } 83 | 84 | public function testIfilter(): void 85 | { 86 | $result = iterator_to_array(it::ifilter(fn($x) => $x & 1, [1, 2, 3, 4])); 87 | $this->assertEquals([1, 3], $result); 88 | } 89 | 90 | public function testImap(): void 91 | { 92 | $result = iterator_to_array(it::imap(fn($x, $y) => $x + $y, [1, 2], [3, 4])); 93 | $this->assertEquals([4, 6], $result); 94 | } 95 | 96 | public function testIzip(): void 97 | { 98 | $result = iterator_to_array(it::izip([1, 2], [3, 4])); 99 | $this->assertEquals([[1, 3], [2, 4]], $result); 100 | } 101 | 102 | public function testIzipLongest(): void 103 | { 104 | $result = iterator_to_array(it::izip_longest([1, 2], [3], 'X')); 105 | $this->assertEquals([[1, 3], [2, 'X']], $result); 106 | } 107 | 108 | public function testPermutations(): void 109 | { 110 | $result = iterator_to_array(it::permutations([1, 2, 3], 2)); 111 | $this->assertEquals([[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]], $result); 112 | 113 | $result = iterator_to_array(it::permutations(range(0, 2))); 114 | $this->assertEquals([[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]], $result); 115 | } 116 | 117 | public function testProduct(): void 118 | { 119 | $result = iterator_to_array(it::product('ABCD', 'xy')); 120 | $this->assertEquals([['A', 'x'], ['A', 'y'], ['B', 'x'], ['B', 'y'], ['C', 'x'], ['C', 'y'], ['D', 'x'], ['D', 'y']], $result); 121 | } 122 | 123 | public function testRepeat(): void 124 | { 125 | $result = iterator_to_array(it::repeat('a', 3)); 126 | $this->assertEquals(['a', 'a', 'a'], $result); 127 | } 128 | 129 | public function testStarmap(): void 130 | { 131 | $result = iterator_to_array(it::starmap(fn($x, $y) => $x ** $y, [[2, 5], [3, 2], [10, 3]])); 132 | $this->assertEquals([32, 9, 1000], $result); 133 | } 134 | 135 | public function testTakewhile(): void 136 | { 137 | $result = iterator_to_array(it::takewhile(fn($x) => $x < 3, [1, 2, 3, 4])); 138 | $this->assertEquals([1, 2], $result); 139 | } 140 | 141 | public function testFilterfalse(): void 142 | { 143 | $result = iterator_to_array(it::filterfalse(fn($x) => $x < 5, [1, 4, 6, 3, 8])); 144 | $this->assertEquals([6, 8], $result); 145 | } 146 | 147 | public function testTee(): void 148 | { 149 | [$it1, $it2] = it::tee([1, 2, 3], 2); 150 | $this->assertEquals([1, 2, 3], iterator_to_array($it1)); 151 | $this->assertEquals([1, 2, 3], iterator_to_array($it2)); 152 | } 153 | 154 | public function testAccumulate(): void 155 | { 156 | $result = it::accumulate([1,2,3,4,5]); 157 | $this->assertEquals([1, 3, 6, 10, 15], iterator_to_array($result)); 158 | $result = it::accumulate([1,2,3,4,5], initial: 100); 159 | $this->assertEquals([100, 101, 103, 106, 110, 115], iterator_to_array($result)); 160 | $result = it::accumulate([3, 4, 6, 2, 1, 9, 0, 7, 5, 8], max(...)); 161 | $this->assertEquals([3, 4, 6, 6, 6, 9, 9, 9, 9, 9], iterator_to_array($result)); 162 | } 163 | 164 | public function testBatched(): void 165 | { 166 | $result = it::batched('ABCDEFG', 3); 167 | $this->assertEquals([['A','B','C'], ['D','E','F'], ['G']], iterator_to_array($result)); 168 | } 169 | 170 | public function testChain_from_iterable(): void 171 | { 172 | $result = it::chain_from_iterable(['ABC', 'DEF']); 173 | $this->assertEquals(['A', 'B', 'C', 'D', 'E', 'F'], iterator_to_array($result)); 174 | $result = it::chain_from_iterable('ABCDEF'); 175 | $this->assertEquals(['A', 'B', 'C', 'D', 'E', 'F'], iterator_to_array($result)); 176 | } 177 | 178 | public function testPairwise(): void 179 | { 180 | $result = it::pairwise('ABCD'); 181 | $this->assertEquals([['A','B'], ['B','C'], ['C','D']], iterator_to_array($result)); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Itertools/Itertools.php: -------------------------------------------------------------------------------- 1 | 0, 23 | 'stop' => $start_or_stop, 24 | 'step' => $step, 25 | ]; 26 | } 27 | 28 | return (object)[ 29 | 'start' => $start_or_stop, 30 | 'stop' => $stop ?? PHP_INT_MAX, 31 | 'step' => $step, 32 | ]; 33 | } 34 | 35 | 36 | public static function iter(string|iterable $var): iterable|IteratorIterator|Iterator|ArrayIterator 37 | { 38 | switch (true) { 39 | case $var instanceof Iterator: 40 | return $var; 41 | 42 | case $var instanceof Traversable: 43 | return new IteratorIterator($var); 44 | 45 | /** @noinspection PhpMissingBreakStatementInspection */ 46 | case is_string($var): 47 | $var = str_split($var); 48 | /* fall-through */ 49 | 50 | case is_array($var): 51 | return new ArrayIterator($var); 52 | 53 | default: 54 | $type = gettype($var); 55 | throw new InvalidArgumentException("'$type' type is not iterable"); 56 | } 57 | } 58 | 59 | public static function enumerate(string|iterable $iterable, int $start = 0): Generator 60 | { 61 | $n = $start; 62 | 63 | foreach (static::iter($iterable) as $value) { 64 | yield [$n, $value]; 65 | $n++; 66 | } 67 | } 68 | 69 | public static function range(int $start_or_stop, int $stop = PHP_INT_MAX, int $step = 1): Generator 70 | { 71 | $args = static::slice(...func_get_args()); 72 | 73 | if ($args->step === 0) { 74 | throw new InvalidArgumentException('range() arg 3 must not be zero'); 75 | } 76 | 77 | if (($args->start > $args->stop && $args->step > 0) || ($args->start < $args->stop && $args->step < 0)) { 78 | return; 79 | } 80 | 81 | for ($i = $args->start; $i !== $args->stop; $i += $args->step) { 82 | yield $i; 83 | } 84 | } 85 | 86 | public static function chain(string|iterable ...$iterables): Generator 87 | { 88 | foreach ($iterables as $it) { 89 | foreach (static::iter($it) as $element) { 90 | yield $element; 91 | } 92 | } 93 | } 94 | 95 | public static function combinations(string|iterable $iterable, int $r): Generator 96 | { 97 | $pool = is_array($iterable) ? $iterable : iterator_to_array(static::iter($iterable)); 98 | $n = count($pool); 99 | 100 | if ($r > $n) { 101 | return; 102 | } 103 | 104 | $indices = range(0, $r - 1); 105 | yield array_slice($pool, 0, $r); 106 | 107 | for (; ;) { 108 | for ($i = $r - 1; $i >= 0; $i--) { 109 | if ($indices[$i] !== $i + $n - $r) { 110 | goto next; 111 | } 112 | } 113 | 114 | return; 115 | 116 | next: 117 | $indices[$i]++; 118 | 119 | for ($j = $i + 1; $j < $r; $j++) { 120 | $indices[$j] = $indices[$j - 1] + 1; 121 | } 122 | 123 | $row = []; 124 | foreach ($indices as $i) { 125 | $row[] = $pool[$i]; 126 | } 127 | 128 | yield $row; 129 | } 130 | } 131 | 132 | public static function combinations_with_replacement(string|iterable $iterable, int $r): Generator 133 | { 134 | $pool = is_array($iterable) ? $iterable : iterator_to_array(static::iter($iterable)); 135 | if (!$pool && $r > 0) { 136 | return; 137 | } 138 | 139 | $n = count($pool); 140 | yield array_fill(0, $r, $pool[0]); 141 | $indices = array_fill(0, $r, 0); 142 | 143 | for (; ;) { 144 | for ($i = $r - 1; $i >= 0; $i--) { 145 | if ($indices[$i] !== $n - 1) { 146 | goto next; 147 | } 148 | } 149 | 150 | return; 151 | 152 | next: 153 | array_splice($indices, $i, count($indices), array_fill(0, $r - $i, $indices[$i] + 1)); 154 | 155 | $row = []; 156 | foreach ($indices as $i) { 157 | $row[] = $pool[$i]; 158 | } 159 | 160 | yield $row; 161 | } 162 | } 163 | 164 | public static function compress(string|iterable $data, string|iterable $selectors): Generator 165 | { 166 | foreach (static::izip($data, $selectors) as [$d, $s]) { 167 | if ($s) { 168 | yield $d; 169 | } 170 | } 171 | } 172 | 173 | public static function count(int $start = 0, int $step = 1): Generator 174 | { 175 | for ($i = $start; ; $i += $step) { 176 | yield $i; 177 | } 178 | } 179 | 180 | public static function cycle(iterable $iterable): InfiniteIterator 181 | { 182 | return new InfiniteIterator(static::iter($iterable)); 183 | } 184 | 185 | public static function dropwhile(callable $predicate, string|iterable $iterable): Generator 186 | { 187 | $found = false; 188 | 189 | foreach (static::iter($iterable) as $x) { 190 | if (!$found && !$predicate($x)) { 191 | $found = true; 192 | } 193 | 194 | if ($found) { 195 | yield $x; 196 | } 197 | } 198 | } 199 | 200 | public static function groupby(string|iterable $iterable, callable $keyfunc = null): Generator 201 | { 202 | $keyfunc ??= static fn($x) => $x; 203 | $iterator = static::iter($iterable); 204 | $exhausted = false; 205 | 206 | while (!$exhausted) { 207 | if ($iterator->valid()) { 208 | $curr_value = $iterator->current(); 209 | $curr_key = $keyfunc($curr_value); 210 | } else { 211 | $exhausted = true; 212 | continue; 213 | } 214 | 215 | $target_key = $curr_key; 216 | $group = []; 217 | 218 | while ($iterator->valid()) { 219 | $curr_value = $iterator->current(); 220 | $curr_key = $keyfunc($curr_value); 221 | 222 | if ($curr_key !== $target_key) { 223 | break; 224 | } 225 | 226 | $group[] = $curr_value; 227 | $iterator->next(); 228 | } 229 | 230 | yield [$target_key, $group]; 231 | 232 | if (!$iterator->valid()) { 233 | $exhausted = true; 234 | } 235 | } 236 | } 237 | 238 | public static function ifilter(?callable $predicate, string|iterable $iterable): Generator 239 | { 240 | if ($predicate === null) { 241 | $predicate = 'boolval'; 242 | } 243 | 244 | foreach (static::iter($iterable) as $x) { 245 | if ($predicate($x)) { 246 | yield $x; 247 | } 248 | } 249 | } 250 | 251 | public static function filterfalse(?callable $predicate, string|iterable $iterable): Generator 252 | { 253 | if ($predicate === null) { 254 | $predicate = 'boolval'; 255 | } 256 | 257 | foreach (static::iter($iterable) as $x) { 258 | if (!$predicate($x)) { 259 | yield $x; 260 | } 261 | } 262 | } 263 | 264 | public static function imap(callable $function = null, string|iterable ...$iterables): Generator 265 | { 266 | foreach (static::izip(...$iterables) as $args) { 267 | if ($function === null) { 268 | yield $args; 269 | } else { 270 | yield $function(...$args); 271 | } 272 | } 273 | } 274 | 275 | public static function islice(string|iterable $iterable, int ...$args): Generator 276 | { 277 | if (static::slice(...$args)->step < 1) { 278 | throw new InvalidArgumentException('Step for islice() must be a positive integer or null.'); 279 | } 280 | 281 | $it = static::range(...$args); 282 | if ($it->valid()) { 283 | $nexti = $it->current(); 284 | 285 | foreach (static::enumerate($iterable) as [$i, $element]) { 286 | if ($i === $nexti) { 287 | yield $element; 288 | 289 | $it->next(); 290 | if (!$it->valid()) { 291 | break; 292 | } 293 | 294 | $nexti = $it->current(); 295 | } 296 | } 297 | } 298 | } 299 | 300 | public static function izip(string|iterable ...$iterables): Generator 301 | { 302 | $multipleIterator = new MultipleIterator(); 303 | foreach ($iterables as $iterable) { 304 | $multipleIterator->attachIterator(static::iter($iterable)); 305 | } 306 | 307 | foreach ($multipleIterator as $item) { 308 | yield $item; 309 | } 310 | } 311 | 312 | public static function izip_longest(/* ...$iterables[, $fillvalue] */ string|iterable ...$args): Generator 313 | { 314 | $fillvalue = array_pop($args); 315 | $counter = count($args); 316 | $iterables = array_map(static::iter(...), $args); 317 | 318 | $sentinel = static function () use (&$counter, $fillvalue) { 319 | $counter--; 320 | yield $fillvalue; 321 | }; 322 | 323 | $fillers = static::repeat($fillvalue); 324 | 325 | $iterators = array_map(static fn($it) => static::chain($it, $sentinel(), $fillers), $iterables); 326 | 327 | for (; ;) { 328 | $row = []; 329 | foreach ($iterators as $iterator) { 330 | if (!$iterator->valid()) { 331 | return; 332 | } 333 | 334 | $row[] = $iterator->current(); 335 | $iterator->next(); 336 | } 337 | 338 | yield $row; 339 | 340 | if (!$counter) { 341 | return; 342 | } 343 | } 344 | } 345 | 346 | public static function permutations(string|iterable $iterable, int $r = null): Generator 347 | { 348 | $pool = is_array($iterable) ? $iterable : iterator_to_array(static::iter($iterable)); 349 | $n = count($pool); 350 | $r ??= $n; 351 | 352 | if ($r > $n) { 353 | return; 354 | } 355 | 356 | $indices = range(0, $n - 1); 357 | $cycles = range($n, $n - $r - 1, -1); 358 | 359 | yield array_slice($pool, 0, $r); 360 | 361 | while ($n) { 362 | for ($i = $r - 1; $i >= 0; $i--) { 363 | $cycles[$i]--; 364 | 365 | if ($cycles[$i] === 0) { 366 | $indices[] = array_splice($indices, $i, 1)[0]; 367 | $cycles[$i] = $n - $i; 368 | } else { 369 | $j = $cycles[$i]; 370 | $minus_j = \count($indices) - $j; 371 | 372 | [$indices[$i], $indices[$minus_j]] = [$indices[$minus_j], $indices[$i]]; 373 | 374 | $row = []; 375 | for ($j = 0; $j < $r; $j++) { 376 | $row[] = $pool[$indices[$j]]; 377 | } 378 | 379 | yield $row; 380 | goto next; 381 | } 382 | } 383 | 384 | return; 385 | next: 386 | } 387 | } 388 | 389 | public static function product(/*...$iterables[, $repeat]*/ string|iterable ...$args): Generator 390 | { 391 | $repeat = is_int($args[array_key_last($args)]) ? array_pop($args) : 1; 392 | $iterables = array_map(static::iter(...), $args); 393 | 394 | $pools = array_merge(...array_fill(0, $repeat, $iterables)); 395 | $result = [[]]; 396 | 397 | foreach ($pools as $pool) { 398 | $result_inner = []; 399 | 400 | foreach ($result as $x) { 401 | foreach ($pool as $y) { 402 | $result_inner[] = array_merge($x, [$y]); 403 | } 404 | } 405 | 406 | $result = $result_inner; 407 | } 408 | 409 | yield from $result; 410 | } 411 | 412 | public static function repeat(mixed $object, int $times = null): Generator 413 | { 414 | if ($times === null) { 415 | for (; ;) { 416 | yield $object; 417 | } 418 | } else { 419 | for ($i = 0; $i < $times; $i++) { 420 | yield $object; 421 | } 422 | } 423 | } 424 | 425 | public static function starmap(callable $function, $iterable): Generator 426 | { 427 | foreach (static::iter($iterable) as $args) { 428 | yield $function(...$args); 429 | } 430 | } 431 | 432 | public static function takewhile(callable $predicate, $iterable): Generator 433 | { 434 | foreach (static::iter($iterable) as $x) { 435 | if ($predicate($x)) { 436 | yield $x; 437 | } else { 438 | break; 439 | } 440 | } 441 | } 442 | 443 | public static function tee(string|iterable $iterable, int $n = 2): array 444 | { 445 | $it = new CachingIterator(static::iter($iterable), CachingIterator::FULL_CACHE); 446 | $result = [$it]; 447 | 448 | for ($i = 1; $i < $n; $i++) { 449 | $result[] = (static function () use ($it) { 450 | foreach ($it->getCache() as $key => $value) { 451 | yield $key => $value; 452 | } 453 | })(); 454 | } 455 | 456 | return $result; 457 | } 458 | 459 | public static function accumulate(string|iterable $iterable, callable $function = null, int $initial = null): Generator 460 | { 461 | $function ??= static fn($a, $b) => $a + $b; 462 | $iterator = self::iter($iterable); 463 | $total = $initial; 464 | 465 | if ($initial === null) { 466 | if ($iterator->valid()) { 467 | $total = $iterator->current(); 468 | $iterator->next(); 469 | } else { 470 | return; 471 | } 472 | } 473 | 474 | yield $total; 475 | 476 | while ($iterator->valid()) { 477 | $total = $function($total, $iterator->current()); 478 | $iterator->next(); 479 | yield $total; 480 | } 481 | } 482 | 483 | public static function batched(string|iterable $iterable, int $n): Generator 484 | { 485 | if ($n < 1) { 486 | throw new InvalidArgumentException("n must be at least one"); 487 | } 488 | 489 | $iterator = self::iter($iterable); 490 | while ($iterator->valid()) { 491 | $batch = []; 492 | for ($i = 0; $i < $n && $iterator->valid(); $i++) { 493 | $batch[] = $iterator->current(); 494 | $iterator->next(); 495 | } 496 | 497 | if ($batch) { 498 | yield $batch; 499 | } 500 | } 501 | } 502 | 503 | public static function chain_from_iterable(string|iterable $iterables): Generator 504 | { 505 | foreach (self::iter($iterables) as $iterable) { 506 | foreach (self::iter($iterable) as $item) { 507 | yield $item; 508 | } 509 | } 510 | } 511 | 512 | public static function pairwise(string|iterable $iterable): Generator 513 | { 514 | $iterator = self::iter($iterable); 515 | 516 | $a = $iterator->current(); 517 | $iterator->next(); 518 | 519 | while ($iterator->valid()) { 520 | $b = $iterator->current(); 521 | yield [$a, $b]; 522 | $a = $b; 523 | $iterator->next(); 524 | } 525 | } 526 | } 527 | 528 | --------------------------------------------------------------------------------