• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sanmai / pipeline / 21350380098

26 Jan 2026 08:02AM UTC coverage: 98.913% (-1.1%) from 100.0%
21350380098

Pull #269

github

web-flow
Merge 5e9db4bd8 into 783e3d871
Pull Request #269: Implement window()

37 of 43 new or added lines in 2 files covered. (86.05%)

1 existing line in 1 file now uncovered.

546 of 552 relevant lines covered (98.91%)

118.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.66
/src/Standard.php
1
<?php
2

3
/**
4
 * Copyright 2017, 2018 Alexey Kopytko <alexey@kopytko.com>
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18

19
declare(strict_types=1);
20

21
namespace Pipeline;
22

23
use ArgumentCountError;
24

25
use function array_chunk;
26
use function array_filter;
27
use function array_flip;
28
use function array_keys;
29
use function array_map;
30
use function array_merge;
31
use function array_reduce;
32
use function array_shift;
33
use function array_slice;
34
use function array_values;
35

36
use ArrayIterator;
37
use CallbackFilterIterator;
38

39
use function count;
40

41
use Countable;
42
use EmptyIterator;
43
use Generator;
44

45
use function is_array;
46
use function is_callable;
47

48
use Iterator;
49

50
use function iterator_count;
51
use function iterator_to_array;
52

53
use IteratorAggregate;
54

55
use function max;
56
use function min;
57
use function mt_getrandmax;
58
use function mt_rand;
59

60
use Override;
61
use Pipeline\Helper\CursorIterator;
62
use Pipeline\Helper\WindowIterator;
63
use Traversable;
64

65
/**
66
 * Concrete pipeline with sensible default callbacks.
67
 *
68
 * @template TKey
69
 * @template TValue
70
 * @template-implements IteratorAggregate<TKey, TValue>
71
 */
72
class Standard implements IteratorAggregate, Countable
73
{
74
    /**
75
     * Pre-primed pipeline.
76
     *
77
     * This is not a full `iterable` per se because we exclude IteratorAggregate before assigning a value.
78
     */
79
    private array|Iterator $pipeline;
80

81
    /**
82
     * Constructor with an optional source of data.
83
     *
84
     * @param null|iterable<TKey, TValue> $input
85
     */
86
    public function __construct(?iterable $input = null)
87
    {
88
        if (null === $input) {
2,658✔
89
            return;
549✔
90
        }
91

92
        $this->replace($input);
2,114✔
93
    }
94

95
    private function replace(iterable $input): void
96
    {
97
        if (is_array($input)) {
2,123✔
98
            $this->pipeline = $input;
831✔
99

100
            return;
831✔
101
        }
102

103
        // IteratorAggregate is a nuance we'd best avoid dealing with.
104
        // For example, CallbackFilterIterator needs a plain Iterator.
105
        while ($input instanceof IteratorAggregate) {
1,334✔
106
            $input = $input->getIterator();
256✔
107
        }
108

109
        /** @var Iterator $input */
110
        $this->pipeline = $input;
1,334✔
111
    }
112

113
    /**
114
     * @psalm-suppress TypeDoesNotContainType
115
     * @phpstan-assert-if-false non-empty-array|Traversable $this->pipeline
116
     */
117
    private function empty(): bool
118
    {
119
        if (!isset($this->pipeline)) {
2,655✔
120
            return true;
201✔
121
        }
122

123
        if ([] === $this->pipeline) {
2,638✔
124
            return true;
219✔
125
        }
126

127
        return false;
2,577✔
128
    }
129

130
    private function discard(): void
131
    {
132
        unset($this->pipeline);
198✔
133
    }
134

135
    /**
136
     * Appends the contents of an iterable to the end of the pipeline.
137
     *
138
     * @param null|iterable<TKey, TValue> $values
139
     *
140
     * @phpstan-self-out self<TKey, TValue>
141
     * @return Standard<TKey, TValue>
142
     */
143
    public function append(?iterable $values = null): self
144
    {
145
        // Do we need to do anything here?
146
        if ($this->willReplace($values)) {
36✔
147
            return $this;
12✔
148
        }
149

150
        return $this->join($this->pipeline, $values);
32✔
151
    }
152

153
    /**
154
     * Appends a list of values to the end of the pipeline.
155
     *
156
     * @param TValue ...$vector
157
     *
158
     * @phpstan-self-out self<TKey, TValue>
159
     * @return Standard<TKey, TValue>
160
     */
161
    public function push(...$vector): self
162
    {
163
        /** @var iterable<array-key, TValue> $vector */
164
        return $this->append($vector);
8✔
165
    }
166

167
    /**
168
     * Prepends the pipeline with the contents of an iterable.
169
     *
170
     * @param null|iterable<TKey, TValue> $values
171
     *
172
     * @phpstan-self-out self<TKey, TValue>
173
     * @return Standard<TKey, TValue>
174
     */
175
    public function prepend(?iterable $values = null): self
176
    {
177
        // Do we need to do anything here?
178
        if ($this->willReplace($values)) {
33✔
179
            return $this;
10✔
180
        }
181

182
        return $this->join($values, $this->pipeline);
31✔
183
    }
184

185
    /**
186
     * Prepends the pipeline with a list of values.
187
     *
188
     * @param TValue ...$vector
189
     *
190
     * @phpstan-self-out self<TKey, TValue>
191
     * @return Standard<TKey, TValue>
192
     */
193
    public function unshift(...$vector): self
194
    {
195
        return $this->prepend($vector);
8✔
196
    }
197

198
    /**
199
     * Determines if the internal pipeline will be replaced when appending/prepending.
200
     *
201
     * Utility method for appending/prepending methods.
202
     *
203
     * @phpstan-assert-if-false iterable $values
204
     */
205
    private function willReplace(?iterable $values = null): bool
206
    {
207
        // Nothing needs to be done here.
208
        if (null === $values || [] === $values) {
69✔
209
            return true;
6✔
210
        }
211

212
        // No shortcuts are applicable if the pipeline was initialized.
213
        if (!$this->empty()) {
69✔
214
            return false;
63✔
215
        }
216

217
        // Handle edge cases in there
218
        $this->replace($values);
20✔
219

220
        return true;
20✔
221
    }
222

223
    /**
224
     * Replaces the internal pipeline with a combination of two non-empty iterables, array-optimized.
225
     *
226
     * Utility method for appending/prepending methods.
227
     *
228
     * @return Standard<TKey, TValue>
229
     */
230
    private function join(iterable $left, iterable $right): self
231
    {
232
        // We got two arrays, that's what we will use.
233
        if (is_array($left) && is_array($right)) {
63✔
234
            $this->pipeline = array_merge($left, $right);
29✔
235

236
            return $this;
29✔
237
        }
238

239
        // Last, join the hard way.
240
        $this->pipeline = self::joinYield($left, $right);
34✔
241

242
        return $this;
34✔
243
    }
244

245
    /**
246
     * Replaces the internal pipeline with a combination of two non-empty iterables, generator-way.
247
     */
248
    private static function joinYield(iterable $left, iterable $right): Generator
249
    {
250
        yield from $left;
34✔
251
        yield from $right;
34✔
252
    }
253

254
    /**
255
     * Flattens inputs: arrays become lists.
256
     * @phpstan-self-out self<array-key, mixed>
257
     * @return Standard<array-key, mixed>
258
     */
259
    public function flatten(): self
260
    {
261
        return $this->map(static function (iterable $args = []) {
7✔
262
            yield from $args;
5✔
263
        });
7✔
264
    }
265

266
    /**
267
     * An extra variant of `map` which unpacks arrays into arguments. Flattens inputs if no callback provided.
268
     *
269
     * @template TUnpackKey
270
     * @template TUnpack
271
     *
272
     * @param null|callable(mixed...): (TUnpack|Generator<TUnpackKey, TUnpack>) $func A callback that accepts any number of arguments and returns a single value.
273
     *
274
     * @phpstan-self-out self<TUnpackKey, TUnpack>
275
     * @return Standard<TUnpackKey, TUnpack>
276
     */
277
    public function unpack(?callable $func = null): self
278
    {
279
        if (null === $func) {
14✔
280
            return $this->flatten();
6✔
281
        }
282

283
        return $this->map(
8✔
284
            /** @param iterable<int|string, mixed> $args */
285
            static function (iterable $args = []) use ($func) {
8✔
286
                return $func(...$args);
8✔
287
            }
8✔
288
        );
8✔
289
    }
290

291
    /**
292
     * Chunks the pipeline into arrays with length elements. The last chunk may contain less than length elements.
293
     *
294
     * @param int<1, max> $length The size of each chunk.
295
     * @param bool $preserve_keys When set to true keys will be preserved. Default is false which will reindex the chunk numerically.
296
     *
297
     * @phpstan-self-out self<array-key, array<TKey, TValue>>
298
     * @return Standard<array-key, array<TKey, TValue>>
299
     */
300
    public function chunk(int $length, bool $preserve_keys = false): self
301
    {
302
        // No-op: an empty array or null.
303
        if ($this->empty()) {
31✔
304
            return $this;
2✔
305
        }
306

307
        // Array shortcut
308
        if (is_array($this->pipeline)) {
29✔
309
            $this->pipeline = array_chunk($this->pipeline, $length, $preserve_keys);
5✔
310

311
            return $this;
5✔
312
        }
313

314
        $this->pipeline = self::toChunks(
24✔
315
            self::makeNonRewindable($this->pipeline),
24✔
316
            $length,
24✔
317
            $preserve_keys
24✔
318
        );
24✔
319

320
        return $this;
24✔
321
    }
322

323
    /**
324
     * @psalm-param positive-int $length
325
     */
326
    private static function toChunks(Generator $input, int $length, bool $preserve_keys): Generator
327
    {
328
        while ($input->valid()) {
24✔
329
            yield iterator_to_array(self::take($input, $length), $preserve_keys);
20✔
330
            $input->next();
20✔
331
        }
332
    }
333

334
    /**
335
     * Chunks the pipeline into arrays with variable sizes. Chunking stops when chunk sizes are exhausted.
336
     *
337
     * @param (iterable<int<0, max>>)|(callable(): iterable<int<0, max>>) $func An iterable or callable that yields chunk sizes. Size 0 produces empty arrays. If callable, it will be invoked to get an iterable.
338
     * @param bool $preserve_keys When set to true keys will be preserved. Default is false which will reindex each chunk numerically.
339
     *
340
     * @phpstan-self-out self<array-key, array<TKey, TValue>>
341
     * @return Standard<array-key, array<TKey, TValue>>
342
     */
343
    public function chunkBy(iterable|callable $func, bool $preserve_keys = false): self
344
    {
345
        // No-op: an empty array or null.
346
        if ($this->empty()) {
45✔
347
            return $this;
2✔
348
        }
349

350
        // Convert callable to iterable
351
        /** @var iterable<int<0, max>> $sizes */
352
        $sizes = is_callable($func) ? $func() : $func;
43✔
353

354
        $this->pipeline = self::toChunksBySize(
43✔
355
            self::makeNonRewindable($this->pipeline),
43✔
356
            $sizes,
43✔
357
            $preserve_keys
43✔
358
        );
43✔
359

360
        return $this;
43✔
361
    }
362

363
    /**
364
     * @param iterable<int<0, max>> $sizes
365
     */
366
    private static function toChunksBySize(Generator $input, iterable $sizes, bool $preserve_keys): Generator
367
    {
368
        foreach ($sizes as $size) {
43✔
369
            if (!$input->valid()) {
42✔
370
                return;
31✔
371
            }
372

373
            if ($size < 1) {
38✔
374
                yield [];
5✔
375
                continue;
5✔
376
            }
377

378
            yield iterator_to_array(self::take($input, $size), $preserve_keys);
38✔
379
            $input->next();
38✔
380
        }
381
    }
382

383
    /**
384
     * Takes a callback that for each input value may return one or yield many. Also takes an initial generator, where it must not require any arguments.
385
     *
386
     * With no callback is a no-op (can safely take a null).
387
     *
388
     * @template TMapKey
389
     * @template TMapValue
390
     *
391
     * @param null|(callable(): (TMapValue|Generator<TMapKey, TMapValue>))|(callable(TValue): (TMapValue|Generator<TMapKey, TMapValue>)) $func A callback must either return a value or yield values (return a generator).
392
     *
393
     * @phpstan-self-out self<TMapKey, TMapValue>
394
     * @return Standard<TMapKey, TMapValue>
395
     */
396
    public function map(?callable $func = null): self
397
    {
398
        if (null === $func) {
545✔
399
            return $this;
1✔
400
        }
401

402
        // That's the standard case for any next stages.
403
        if (isset($this->pipeline)) {
545✔
404
            $this->pipeline = self::apply($this->pipeline, $func);
43✔
405

406
            return $this;
43✔
407
        }
408

409
        // Let's check what we got for a start.
410
        $value = $func();
520✔
411

412
        // Generator is a generator, moving along
413
        if ($value instanceof Generator) {
517✔
414
            // It is possible to detect if callback is a generator like so:
415
            // (new \ReflectionFunction($func))->isGenerator();
416
            // Yet this will restrict users from replacing the pipeline and has unknown performance impact.
417
            // But, again, we could add a direct internal method to replace the pipeline, e.g. as done by unpack()
418
            $this->pipeline = $value;
507✔
419

420
            return $this;
507✔
421
        }
422

423
        // Not a generator means we were given a simple value to be treated as an array.
424
        // We do not cast to an array here because casting a null to an array results in
425
        // an empty array; that's surprising and not how it works for other values.
426
        $this->pipeline = [
10✔
427
            $value,
10✔
428
        ];
10✔
429

430
        return $this;
10✔
431
    }
432

433
    private static function apply(iterable $previous, callable $func): Generator
434
    {
435
        foreach ($previous as $key => $value) {
38✔
436
            $result = $func($value);
35✔
437

438
            // For generators we use keys they provide
439
            if ($result instanceof Generator) {
34✔
440
                yield from $result;
15✔
441

442
                continue;
15✔
443
            }
444

445
            // In case of a plain old mapping function we use the original key
446
            yield $key => $result;
22✔
447
        }
448
    }
449

450
    /**
451
     * Takes a callback that for each input value is expected to return another single value. Unlike map(), it assumes no special treatment for generators.
452
     *
453
     * With no callback is a no-op (can safely take a null).
454
     *
455
     * @template TCast
456
     *
457
     * @param null|(callable(TValue): TCast)|(callable(): TCast) $func A callback must return a value.
458
     *
459
     * @phpstan-self-out self<TKey, TCast>
460
     * @return Standard<TKey, TCast>
461
     */
462
    public function cast(?callable $func = null): self
463
    {
464
        if (null === $func) {
20✔
465
            return $this;
1✔
466
        }
467

468
        // We got an array, that's what we need. Moving along.
469
        if (isset($this->pipeline) && is_array($this->pipeline)) {
19✔
470
            $this->pipeline = array_map($func, $this->pipeline);
5✔
471

472
            return $this;
5✔
473
        }
474

475
        if (isset($this->pipeline)) {
14✔
476
            $this->pipeline = self::applyOnce($this->pipeline, $func);
13✔
477

478
            return $this;
13✔
479
        }
480

481
        // Else get the seed value.
482
        // We do not cast to an array here because casting a null to an array results in
483
        // an empty array; that's surprising and not how it works for other values.
484
        $this->pipeline = [
1✔
485
            $func(),
1✔
486
        ];
1✔
487

488
        return $this;
1✔
489
    }
490

491
    private static function applyOnce(iterable $previous, callable $func): Generator
492
    {
493
        foreach ($previous as $key => $value) {
13✔
494
            yield $key => $func($value);
13✔
495
        }
496
    }
497

498
    /**
499
     * Selects elements for which the callback returns true.
500
     *
501
     * With no callback drops all null and false values (not unlike array_filter does by default).
502
     *
503
     * @param null|callable(TValue): bool $func A callback that accepts a single value and returns a boolean value.
504
     * @param bool $strict When true, only `null` and `false` are filtered out.
505
     * @param null|callable(TValue, TKey=): void $onReject Optional callback for rejected items (side effects like logging).
506
     *
507
     * @phpstan-self-out self<TKey, TValue>
508
     * @return Standard<TKey, TValue>
509
     */
510
    public function select(?callable $func = null, bool $strict = true, ?callable $onReject = null): self
511
    {
512
        // No-op: an empty array or null.
513
        if ($this->empty()) {
42✔
514
            return $this;
3✔
515
        }
516

517
        $func = self::resolvePredicate($func, $strict);
41✔
518

519
        // When onReject callback is provided, use generator path for side effects.
520
        if (null !== $onReject) {
41✔
521
            $this->pipeline = self::selectWithRejectCallback($this->pipeline, $func, $onReject);
6✔
522

523
            return $this;
6✔
524
        }
525

526
        // We got an array, that's what we need. Moving along.
527
        if (is_array($this->pipeline)) {
35✔
528
            $this->pipeline = array_filter($this->pipeline, $func);
11✔
529

530
            return $this;
11✔
531
        }
532

533
        $this->pipeline = new CallbackFilterIterator($this->pipeline, $func);
24✔
534

535
        return $this;
24✔
536
    }
537

538
    private static function selectWithRejectCallback(iterable $previous, callable $predicate, callable $onReject): Generator
539
    {
540
        foreach ($previous as $key => $value) {
6✔
541
            if ($predicate($value)) {
6✔
542
                yield $key => $value;
6✔
543

544
                continue;
6✔
545
            }
546

547
            self::callWithValueKey($onReject, $value, $key);
6✔
548
        }
549
    }
550

551
    /**
552
     * Removes elements unless a callback returns true. Alias for select().
553
     *
554
     * With no callback drops all null and false values (not unlike array_filter does by default).
555
     *
556
     * @see select()
557
     *
558
     * @param null|callable(TValue): bool $func A callback that accepts a single value and returns a boolean value.
559
     * @param bool $strict When true, only `null` and `false` are filtered out.
560
     *
561
     * @phpstan-self-out self<TKey, TValue>
562
     * @return Standard<TKey, TValue>
563
     */
564
    public function filter(?callable $func = null, bool $strict = false): self
565
    {
566
        return $this->select($func, $strict);
32✔
567
    }
568

569
    /**
570
     * Resolves a nullable predicate into a sensible non-null callable.
571
     */
572
    private static function resolvePredicate(?callable $func, bool $strict): callable
573
    {
574
        if (null === $func && $strict) {
41✔
575
            return self::strictPredicate(...);
3✔
576
        }
577

578
        if (null === $func) {
38✔
579
            return self::nonStrictPredicate(...);
17✔
580
        }
581

582
        // Handle strict mode for user provided predicates.
583
        if ($strict) {
22✔
584
            return static function ($value) use ($func) {
7✔
585
                return self::strictPredicate($func($value));
7✔
586
            };
7✔
587
        }
588

589
        return self::resolveStringPredicate($func);
15✔
590
    }
591

592
    private static function strictPredicate(mixed $value): bool
593
    {
594
        return null !== $value && false !== $value;
10✔
595
    }
596

597
    private static function nonStrictPredicate(mixed $value): bool
598
    {
599
        return (bool) $value;
11✔
600
    }
601

602
    /**
603
     * Resolves a string/callable predicate into a sensible non-null callable.
604
     */
605
    private static function resolveStringPredicate(callable $func): callable
606
    {
607
        // Make sure we pass only one argument the callback, as CallbackFilterIterator provides three
608
        return static fn($value) => $func($value);
15✔
609
    }
610

611
    /**
612
     * Skips elements while the predicate returns true, and keeps everything after the predicate returns false just once.
613
     *
614
     * @param callable(TValue): bool $predicate A callback returning boolean value.
615
     *
616
     * @phpstan-self-out self<TKey, TValue>
617
     * @return Standard<TKey, TValue>
618
     */
619
    public function skipWhile(callable $predicate): self
620
    {
621
        // No-op: an empty array or null.
622
        if ($this->empty()) {
5✔
623
            return $this;
1✔
624
        }
625

626
        $predicate = self::resolveStringPredicate($predicate);
4✔
627

628
        $this->filter(static function ($value) use ($predicate): bool {
4✔
629
            static $done = false;
4✔
630

631
            if ($predicate($value) && !$done) {
4✔
632
                return false;
3✔
633
            }
634

635
            $done = true;
4✔
636

637
            return true;
4✔
638
        });
4✔
639

640
        return $this;
4✔
641
    }
642

643
    /**
644
     * Reduces input values to a single value. Defaults to summation. This is a terminal operation.
645
     *
646
     * @template T
647
     *
648
     * @param ?callable $func A reducer such as fn($carry, $item), must return updated $carry.
649
     * @param T $initial The initial value for the $carry.
650
     *
651
     * @return int|T
652
     */
653
    public function reduce(?callable $func = null, $initial = null)
654
    {
655
        return $this->fold($initial ?? 0, $func);
19✔
656
    }
657

658
    /**
659
     * Reduces input values to a single value. Defaults to summation. Requires an initial value. This is a terminal operation.
660
     *
661
     * @template T
662
     *
663
     * @param T $initial Initial value for the $carry.
664
     * @param ?callable $func A reducer such as fn($carry, $item), must return updated $carry.
665
     *
666
     * @return T
667
     */
668
    public function fold($initial, ?callable $func = null)
669
    {
670
        if ($this->empty()) {
20✔
671
            return $initial;
1✔
672
        }
673

674
        $func ??= self::defaultReducer(...);
19✔
675

676
        if (is_array($this->pipeline)) {
19✔
677
            return array_reduce($this->pipeline, $func, $initial);
5✔
678
        }
679

680
        foreach ($this->pipeline as $value) {
14✔
681
            $initial = $func($initial, $value);
13✔
682
        }
683

684
        return $initial;
14✔
685
    }
686

687
    /**
688
     * @param mixed $carry
689
     * @param mixed $item
690
     * @return mixed
691
     */
692
    private static function defaultReducer($carry, $item)
693
    {
694
        $carry += $item;
17✔
695

696
        return $carry;
17✔
697
    }
698

699
    /**
700
     * {@inheritdoc}
701
     *
702
     * We stripped IteratorAggregate in the constructor/replace(),
703
     * so this is guaranteed to be an Iterator.
704
     *
705
     * @return Iterator<TKey, TValue>
706
     */
707
    #[Override]
708
    public function getIterator(): Traversable
709
    {
710
        if (!isset($this->pipeline)) {
867✔
711
            return new EmptyIterator();
1✔
712
        }
713

714
        if ($this->pipeline instanceof Traversable) {
866✔
715
            return $this->pipeline;
853✔
716
        }
717

718
        return new ArrayIterator($this->pipeline);
30✔
719
    }
720

721
    /**
722
     * Returns a forward-only iterator that maintains position across iterations.
723
     * @return Iterator<TKey, TValue>
724
     */
725
    public function cursor(): Iterator
726
    {
727
        if ($this->empty()) {
50✔
728
            return new EmptyIterator();
1✔
729
        }
730

731
        $iterator = $this->getIterator();
49✔
732

733
        // Avoid double wrapping
734
        if ($iterator instanceof CursorIterator) {
49✔
735
            return $iterator;
1✔
736
        }
737

738
        /** @var Iterator $iterator */
739
        return new CursorIterator($iterator);
49✔
740
    }
741

742
    /**
743
     * Returns a rewindable iterator that caches elements for replay.
744
     *
745
     * Unlike cursor() which is forward-only, window() buffers seen elements
746
     * allowing rewind within the buffer bounds.
747
     *
748
     * With a size limit, oldest elements are dropped (sliding window).
749
     *
750
     * @param int<1, max>|null $size Maximum buffer size (null = unlimited)
751
     * @return Iterator<TKey, TValue>
752
     */
753
    public function window(?int $size = null): Iterator
754
    {
NEW
755
        if ($this->empty()) {
×
NEW
756
            return new EmptyIterator();
×
757
        }
758

NEW
759
        $iterator = $this->getIterator();
×
760

761
        // Avoid double wrapping
NEW
762
        if ($iterator instanceof WindowIterator) {
×
NEW
763
            return $iterator;
×
764
        }
765

766
        /** @var Iterator $iterator */
NEW
UNCOV
767
        return new WindowIterator($iterator, $size);
×
768
    }
769

770
    /**
771
     * By default, returns all values regardless of keys used, discarding all keys in the process. This is a terminal operation.
772
     * @return list<TValue>
773
     */
774
    public function toList(): array
775
    {
776
        // No-op: an empty array or null.
777
        if ($this->empty()) {
722✔
778
            return [];
219✔
779
        }
780

781
        // We got what we need, moving along.
782
        if (is_array($this->pipeline)) {
529✔
783
            return array_values($this->pipeline);
184✔
784
        }
785

786
        // Because `yield from` does not reset keys we have to ignore them on export by default to return every item.
787
        // http://php.net/manual/en/language.generators.syntax.php#control-structures.yield.from
788
        return iterator_to_array($this, preserve_keys: false);
370✔
789
    }
790

791
    /**
792
     * @deprecated Use toList() or toAssoc() instead.
793
     */
794
    public function toArray(bool $preserve_keys): array
795
    {
796
        if ($preserve_keys) {
1,044✔
797
            return $this->toAssoc();
468✔
798
        }
799

800
        return $this->toList();
576✔
801
    }
802

803
    /**
804
     * Returns all values preserving keys. This is a terminal operation.
805
     *
806
     * @return array<TKey, TValue>
807
     */
808
    public function toAssoc(): array
809
    {
810
        // No-op: an empty array or null.
811
        if ($this->empty()) {
583✔
812
            return [];
167✔
813
        }
814

815
        // We got what we need, moving along.
816
        if (is_array($this->pipeline)) {
416✔
817
            return $this->pipeline;
127✔
818
        }
819

820
        // Preserve keys by default.
821
        return iterator_to_array($this);
289✔
822
    }
823

824
    /**
825
     * Counts seen values online.
826
     *
827
     * @param ?int &$count The current count; initialized unless provided.
828
     *
829
     * @param-out int $count
830
     *
831
     * @phpstan-self-out self<TKey, TValue>
832
     * @return Standard<TKey, TValue>
833
     */
834
    public function runningCount(
835
        ?int &$count
836
    ): self {
837
        $count ??= 0;
3✔
838

839
        $this->cast(static function ($input) use (&$count) {
3✔
840
            ++$count;
3✔
841

842
            return $input;
3✔
843
        });
3✔
844

845
        return $this;
3✔
846
    }
847

848
    /**
849
     * {@inheritdoc}
850
     *
851
     * This is a terminal operation.
852
     *
853
     * @see Countable::count()
854
     */
855
    #[Override]
856
    public function count(): int
857
    {
858
        if ($this->empty()) {
13✔
859
            // With non-primed pipeline just return zero.
860
            return 0;
4✔
861
        }
862

863
        if (is_array($this->pipeline)) {
9✔
864
            return count($this->pipeline);
3✔
865
        }
866

867
        return iterator_count($this->pipeline);
6✔
868
    }
869

870
    /**
871
     * Converts the pipeline to a non-rewindable stream.
872
     *
873
     * @phpstan-self-out self<TKey, TValue>
874
     * @return Standard<TKey, TValue>
875
     */
876
    public function stream()
877
    {
878
        $this->pipeline = self::makeNonRewindable($this->pipeline ?? []);
7✔
879

880
        return $this;
7✔
881
    }
882

883
    private static function makeNonRewindable(iterable $input): Generator
884
    {
885
        if ($input instanceof Generator) {
1,559✔
886
            return $input;
936✔
887
        }
888

889
        return self::generatorFromIterable($input);
626✔
890
    }
891

892
    private static function generatorFromIterable(iterable $input): Generator
893
    {
894
        yield from $input;
626✔
895
    }
896

897
    /**
898
     * Returns the first N items from the pipeline as a new pipeline, removing them from the current pipeline (destructive).
899
     * Users can call prepend() to restore items if non-destructive behavior is needed.
900
     *
901
     * @param int<0, max> $count Number of items to peek at.
902
     *
903
     * @return self<TKey, TValue> Pipeline (new instance) of peeked items with keys preserved (including duplicate keys).
904
     */
905
    public function peek(int $count): self
906
    {
907
        return take($this->peekAsIterable($count));
50✔
908
    }
909

910
    private function peekAsIterable(int $count): iterable
911
    {
912
        // No-op: empty pipeline or zero count
913
        if ($this->empty() || $count <= 0) {
50✔
914
            return [];
14✔
915
        }
916

917
        // Fast-path for arrays
918
        if (is_array($this->pipeline)) {
36✔
919
            $peeked = array_slice($this->pipeline, 0, $count, true);
11✔
920
            $this->pipeline = array_slice($this->pipeline, $count, null, true);
11✔
921

922
            return $peeked;
11✔
923
        }
924

925
        // Convert to non-rewindable iterator
926
        $generator = self::makeNonRewindable($this->pipeline);
25✔
927

928
        // Collect items eagerly (to update pipeline state before returning)
929
        // And preserve duplicates as tuples
930
        $peeked = iterator_to_array(self::toTuples(self::take($generator, $count)));
25✔
931

932
        // Wrap remaining items in a fresh generator to avoid rewind issues
933
        $this->pipeline = self::resumeGenerator($generator);
25✔
934

935

936
        // Return generator that yields the collected items
937
        return self::tuplesToGenerator($peeked);
25✔
938
    }
939

940
    /**
941
     * Advances the pointer to counter the optimizations of self::take(), while also deferring the costs.
942
     */
943
    private static function resumeGenerator(Generator $input): Generator
944
    {
945
        $input->next();
24✔
946

947
        while ($input->valid()) {
24✔
948
            yield $input->key() => $input->current();
12✔
949
            // @infection-ignore-all causes infinite loops
950
            $input->next();
12✔
951
        }
952
    }
953

954
    private static function tuplesToGenerator(iterable $input): Generator
955
    {
956
        foreach ($input as [$key, $value]) {
25✔
957
            yield $key => $value;
21✔
958
        }
959
    }
960

961
    /**
962
     * Extracts a slice from the inputs. Keys are not discarded intentionally.
963
     *
964
     * @see \array_slice()
965
     *
966
     * @param int  $offset If offset is non-negative, the sequence will start at that offset. If offset is negative, the sequence will start that far from the end.
967
     * @param ?int $length If length is given and is positive, then the sequence will have up to that many elements in it. If length is given and is negative then the sequence will stop that many elements from the end.
968
     *
969
     * @phpstan-self-out self<TKey, TValue>
970
     * @return Standard<TKey, TValue>
971
     */
972
    public function slice(int $offset, ?int $length = null)
973
    {
974
        if ($this->empty()) {
888✔
975
            // With non-primed pipeline just move along.
976
            return $this;
50✔
977
        }
978

979
        if (0 === $length) {
838✔
980
            // We're not consuming anything assuming total laziness.
981
            $this->discard();
172✔
982

983
            return $this;
172✔
984
        }
985

986
        // Shortcut to array_slice() for actual arrays.
987
        if (is_array($this->pipeline)) {
666✔
988
            $this->pipeline = array_slice($this->pipeline, $offset, $length, true);
310✔
989

990
            return $this;
310✔
991
        }
992

993
        $this->pipeline = self::sliceToIterator(
356✔
994
            self::makeNonRewindable($this->pipeline),
356✔
995
            $offset,
356✔
996
            $length
356✔
997
        );
356✔
998

999
        return $this;
356✔
1000
    }
1001

1002
    private static function sliceToIterator(Iterator $stream, int $offset, ?int $length): Iterator
1003
    {
1004
        if ($offset < 0) {
356✔
1005
            // If offset is negative, the sequence will start that far from the end of the array.
1006
            $stream = self::tail($stream, -$offset);
149✔
1007
        }
1008

1009
        if ($offset > 0) {
356✔
1010
            // If offset is non-negative, the sequence will start at that offset in the array.
1011
            $stream = self::skip($stream, $offset);
113✔
1012
        }
1013

1014
        if ($length < 0) {
356✔
1015
            // If length is given and is negative then the sequence will stop that many elements from the end of the array.
1016
            $stream = self::head($stream, -$length);
165✔
1017
        }
1018

1019
        if ($length > 0) {
356✔
1020
            // If length is given and is positive, then the sequence will have up to that many elements in it.
1021
            $stream = self::take($stream, $length);
116✔
1022
        }
1023

1024
        return $stream;
356✔
1025
    }
1026

1027
    /**
1028
     * @psalm-param positive-int $skip
1029
     */
1030
    private static function skip(Iterator $input, int $skip): Iterator
1031
    {
1032
        // Consume until seen enough.
1033
        foreach ($input as $_) {
113✔
1034
            /** @psalm-suppress DocblockTypeContradiction */
1035
            if (0 === $skip--) {
101✔
1036
                break;
53✔
1037
            }
1038
        }
1039

1040
        // Avoid yielding from an exhausted generator. Gives error:
1041
        // Generator passed to yield from was aborted without proper return and is unable to continue
1042
        if (!$input->valid()) {
113✔
1043
            return;
63✔
1044
        }
1045

1046
        yield from $input;
53✔
1047
    }
1048

1049
    /**
1050
     * Note: it does not call next() upon stopping - caller's responsibility to do that if they want to reuse the iterator.
1051
     * @psalm-param positive-int $take
1052
     */
1053
    private static function take(Iterator $input, int $take): Iterator
1054
    {
1055
        while ($input->valid()) {
238✔
1056
            yield $input->key() => $input->current();
206✔
1057

1058
            // Stop once taken enough.
1059
            if (0 === --$take) {
206✔
1060
                break;
146✔
1061
            }
1062

1063
            $input->next();
189✔
1064
        }
1065
    }
1066

1067
    private static function tail(iterable $input, int $length): Iterator
1068
    {
1069
        $buffer = [];
149✔
1070

1071
        foreach ($input as $key => $value) {
149✔
1072
            if (count($buffer) < $length) {
133✔
1073
                // Read at most N records.
1074
                $buffer[] = [$key, $value];
133✔
1075

1076
                continue;
133✔
1077
            }
1078

1079
            // Remove and add one record each time.
1080
            array_shift($buffer);
79✔
1081
            $buffer[] = [$key, $value];
79✔
1082
        }
1083

1084
        foreach ($buffer as [$key, $value]) {
149✔
1085
            yield $key => $value;
133✔
1086
        }
1087
    }
1088

1089
    /**
1090
     * Allocates a buffer of $length, and reads records into it, proceeding with FIFO when buffer is full.
1091
     */
1092
    private static function head(iterable $input, int $length): Iterator
1093
    {
1094
        $buffer = [];
165✔
1095

1096
        foreach ($input as $key => $value) {
165✔
1097
            $buffer[] = [$key, $value];
131✔
1098

1099
            if (count($buffer) > $length) {
131✔
1100
                [$key, $value] = array_shift($buffer);
65✔
1101
                yield $key => $value;
65✔
1102
            }
1103
        }
1104
    }
1105

1106
    /**
1107
     * Performs a lazy zip operation on iterables, not unlike that of
1108
     * array_map with first argument set to null. Also known as transposition.
1109
     *
1110
     * @param iterable<mixed> ...$inputs
1111
     * @phpstan-self-out self<TKey, array{TValue, ...}>
1112
     * @return Standard<TKey, array{TValue, ...}>
1113
     */
1114
    public function zip(iterable ...$inputs)
1115
    {
1116
        if ([] === $inputs) {
13✔
1117
            return $this;
1✔
1118
        }
1119

1120
        if (!isset($this->pipeline)) {
12✔
1121
            $this->replace(array_shift($inputs));
3✔
1122
        }
1123

1124
        if ([] === $inputs) {
12✔
1125
            return $this;
2✔
1126
        }
1127

1128
        $this->map(static function ($item): array {
10✔
1129
            return [$item];
9✔
1130
        });
10✔
1131

1132
        foreach (self::toIterators(...$inputs) as $iterator) {
10✔
1133
            // MultipleIterator won't work here because it'll stop at first invalid iterator.
1134
            $this->map(static function (array $current) use ($iterator) {
10✔
1135
                if (!$iterator->valid()) {
9✔
1136
                    $current[] = null;
2✔
1137

1138
                    return $current;
2✔
1139
                }
1140

1141
                $current[] = $iterator->current();
9✔
1142
                $iterator->next();
9✔
1143

1144
                return $current;
9✔
1145
            });
10✔
1146
        }
1147

1148
        return $this;
10✔
1149
    }
1150

1151
    /**
1152
     * @return Iterator[]
1153
     */
1154
    private static function toIterators(iterable ...$inputs): array
1155
    {
1156
        return array_map(static function (iterable $input): Iterator {
10✔
1157
            while ($input instanceof IteratorAggregate) {
10✔
1158
                $input = $input->getIterator();
3✔
1159
            }
1160

1161
            if ($input instanceof Iterator) {
10✔
1162
                return $input;
5✔
1163
            }
1164

1165
            // IteratorAggregate and Iterator are out of picture, which leaves... an array.
1166

1167
            /** @var array $input */
1168
            return new ArrayIterator($input);
6✔
1169
        }, $inputs);
10✔
1170
    }
1171

1172
    /**
1173
     * Performs reservoir sampling with an optional weighting function. Uses the most optimal algorithm.
1174
     *
1175
     * @see https://en.wikipedia.org/wiki/Reservoir_sampling
1176
     *
1177
     * @param int       $size       The desired sample size.
1178
     * @param ?callable $weightFunc The optional weighting function.
1179
     */
1180
    public function reservoir(int $size, ?callable $weightFunc = null): array
1181
    {
1182
        if ($this->empty()) {
52✔
1183
            return [];
1✔
1184
        }
1185

1186
        if ($size <= 0) {
51✔
1187
            // Discard the state to emulate full consumption
1188
            $this->discard();
12✔
1189

1190
            return [];
12✔
1191
        }
1192

1193
        // Algorithms below assume inputs are non-rewindable
1194
        $this->pipeline = self::makeNonRewindable($this->pipeline);
39✔
1195

1196
        $result = null === $weightFunc ?
39✔
1197
            self::reservoirRandom($this->pipeline, $size) :
24✔
1198
            self::reservoirWeighted($this->pipeline, $size, $weightFunc);
15✔
1199

1200
        return iterator_to_array($result, true);
39✔
1201
    }
1202

1203
    private static function drainValues(Generator $input): Generator
1204
    {
1205
        while ($input->valid()) {
27✔
1206
            yield $input->current();
27✔
1207
            // @infection-ignore-all
1208
            $input->next();
27✔
1209
        }
1210
    }
1211

1212
    /**
1213
     * Implements the simple and slow algorithm, commonly known as Algorithm R.
1214
     *
1215
     * @see https://en.wikipedia.org/wiki/Reservoir_sampling#Simple_algorithm
1216
     *
1217
     * @psalm-param positive-int $size
1218
     */
1219
    private static function reservoirRandom(Generator $input, int $size): Generator
1220
    {
1221
        // Take an initial sample (AKA fill the reservoir array)
1222
        foreach (self::take($input, $size) as $output) {
24✔
1223
            yield $output;
24✔
1224
        }
1225

1226
        // Fetch the next value
1227
        $input->next();
24✔
1228

1229
        // Return if there's nothing more to fetch
1230
        if (!$input->valid()) {
24✔
1231
            return;
6✔
1232
        }
1233

1234
        $counter = $size;
18✔
1235

1236
        // Produce replacement elements with gradually decreasing probability
1237
        foreach (self::drainValues($input) as $value) {
18✔
1238
            $key = mt_rand(0, $counter);
18✔
1239

1240
            if ($key < $size) {
18✔
1241
                yield $key => $value;
15✔
1242
            }
1243

1244
            ++$counter;
18✔
1245
        }
1246
    }
1247

1248
    /**
1249
     * Performs weighted random sampling.
1250
     *
1251
     * @see https://en.wikipedia.org/wiki/Reservoir_sampling#Algorithm_A-Chao
1252
     *
1253
     * @psalm-param positive-int $size
1254
     */
1255
    private static function reservoirWeighted(Generator $input, int $size, callable $weightFunc): Generator
1256
    {
1257
        $sum = 0.0;
15✔
1258

1259
        // Take an initial sample (AKA fill the reservoir array)
1260
        foreach (self::take($input, $size) as $output) {
15✔
1261
            yield $output;
15✔
1262
            $sum += $weightFunc($output);
15✔
1263
        }
1264

1265
        // Fetch the next value
1266
        $input->next();
15✔
1267

1268
        // Return if there's nothing more to fetch
1269
        if (!$input->valid()) {
15✔
1270
            return;
6✔
1271
        }
1272

1273
        foreach (self::drainValues($input) as $value) {
9✔
1274
            $weight = $weightFunc($value);
9✔
1275
            $sum += $weight;
9✔
1276

1277
            // probability for this item
1278
            $probability = $weight / $sum;
9✔
1279

1280
            // @infection-ignore-all
1281
            if (self::random() <= $probability) {
9✔
1282
                yield mt_rand(0, $size - 1) => $value;
9✔
1283
            }
1284
        }
1285
    }
1286

1287
    /**
1288
     * Returns a pseudorandom value between zero (inclusive) and one (exclusive).
1289
     *
1290
     * @infection-ignore-all
1291
     */
1292
    private static function random(): float
1293
    {
1294
        return mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax();
9✔
1295
    }
1296

1297
    /**
1298
     * Finds the lowest value using the standard comparison rules. Returns null for empty sequences.
1299
     *
1300
     * @return null|TValue
1301
     */
1302
    public function min()
1303
    {
1304
        if ($this->empty()) {
624✔
1305
            return null;
2✔
1306
        }
1307

1308
        if (is_array($this->pipeline)) {
622✔
1309
            /** @psalm-suppress ArgumentTypeCoercion */
1310
            return min($this->pipeline);
88✔
1311
        }
1312

1313
        $this->pipeline = self::makeNonRewindable($this->pipeline);
534✔
1314

1315
        $min = null;
534✔
1316

1317
        foreach ($this->pipeline as $min) {
534✔
1318
            break;
528✔
1319
        }
1320

1321
        // Return if there's nothing more to fetch
1322
        if (!$this->pipeline->valid()) {
534✔
1323
            return $min;
6✔
1324
        }
1325

1326
        foreach ($this->pipeline as $value) {
528✔
1327
            if ($value < $min) {
528✔
1328
                $min = $value;
306✔
1329
            }
1330
        }
1331

1332
        return $min;
528✔
1333
    }
1334

1335
    /**
1336
     * Finds the highest value using the standard comparison rules. Returns null for empty sequences.
1337
     *
1338
     * @return null|TValue
1339
     */
1340
    public function max()
1341
    {
1342
        if ($this->empty()) {
624✔
1343
            return null;
2✔
1344
        }
1345

1346
        if (is_array($this->pipeline)) {
622✔
1347
            /** @psalm-suppress ArgumentTypeCoercion */
1348
            return max($this->pipeline);
88✔
1349
        }
1350

1351
        $this->pipeline = self::makeNonRewindable($this->pipeline);
534✔
1352

1353
        // Everything is greater than null
1354
        $max = null;
534✔
1355

1356
        foreach ($this->pipeline as $value) {
534✔
1357
            if ($value > $max) {
528✔
1358
                $max = $value;
516✔
1359
            }
1360
        }
1361

1362
        return $max;
534✔
1363
    }
1364

1365
    /**
1366
     * Returns the first element in the pipeline. This is a terminal operation.
1367
     *
1368
     * @return null|TValue
1369
     */
1370
    public function first()
1371
    {
1372
        if ($this->empty()) {
10✔
1373
            return null;
2✔
1374
        }
1375

1376
        foreach ($this->pipeline as $value) {
8✔
1377
            return $value;
7✔
1378
        }
1379

1380
        return null;
1✔
1381
    }
1382

1383
    /**
1384
     * Returns the last element in the pipeline. This is a terminal operation.
1385
     *
1386
     * @return null|TValue
1387
     */
1388
    public function last()
1389
    {
1390
        if ($this->empty()) {
10✔
1391
            return null;
2✔
1392
        }
1393

1394
        foreach ($this->pipeline as $value) {
8✔
1395
            // Nothing
1396
        }
1397

1398
        return $value ?? null;
8✔
1399
    }
1400

1401
    /**
1402
     * Extracts only the values from the pipeline, discarding keys.
1403
     *
1404
     * @phpstan-self-out self<TKey, TValue>
1405
     * @return Standard<TKey, TValue>
1406
     */
1407
    public function values()
1408
    {
1409
        if ($this->empty()) {
4✔
1410
            // No-op: null.
1411
            return $this;
1✔
1412
        }
1413

1414
        if (is_array($this->pipeline)) {
3✔
1415
            $this->pipeline = array_values($this->pipeline);
1✔
1416

1417
            return $this;
1✔
1418
        }
1419

1420
        $this->pipeline = self::valuesOnly($this->pipeline);
2✔
1421

1422
        return $this;
2✔
1423
    }
1424

1425
    private static function valuesOnly(iterable $previous): Generator
1426
    {
1427
        foreach ($previous as $value) {
2✔
1428
            yield $value;
2✔
1429
        }
1430
    }
1431

1432
    /**
1433
     * Extracts only the keys from the pipeline, discarding values.
1434
     *
1435
     * @phpstan-self-out self<array-key, TKey>
1436
     * @return Standard<array-key, TKey>
1437
     */
1438
    public function keys()
1439
    {
1440
        if ($this->empty()) {
4✔
1441
            // No-op: null.
1442
            return $this;
1✔
1443
        }
1444

1445
        if (is_array($this->pipeline)) {
3✔
1446
            $this->pipeline = array_keys($this->pipeline);
1✔
1447

1448
            return $this;
1✔
1449
        }
1450

1451
        $this->pipeline = self::keysOnly($this->pipeline);
2✔
1452

1453
        return $this;
2✔
1454
    }
1455

1456
    private static function keysOnly(iterable $previous): Generator
1457
    {
1458
        foreach ($previous as $key => $_) {
2✔
1459
            yield $key;
2✔
1460
        }
1461
    }
1462

1463
    /**
1464
     * Swaps keys and values in the pipeline.
1465
     * The new values will be the original keys, and the new keys will be the original values.
1466
     *
1467
     * @phpstan-self-out self<TValue, TKey>
1468
     * @return Standard<TValue, TKey>
1469
     */
1470
    public function flip()
1471
    {
1472
        if ($this->empty()) {
98✔
1473
            // No-op: null.
1474
            return $this;
2✔
1475
        }
1476

1477
        if (is_array($this->pipeline)) {
96✔
1478
            $this->pipeline = array_flip($this->pipeline);
18✔
1479

1480
            return $this;
18✔
1481
        }
1482

1483
        $this->pipeline = self::flipKeysAndValues($this->pipeline);
78✔
1484

1485
        return $this;
78✔
1486
    }
1487

1488
    private static function flipKeysAndValues(iterable $previous): Generator
1489
    {
1490
        foreach ($previous as $key => $value) {
78✔
1491
            yield $value => $key;
74✔
1492
        }
1493
    }
1494

1495
    /**
1496
     * Converts each key-value pair into a tuple [key, value].
1497
     *
1498
     * @phpstan-self-out self<array-key, array{0: TKey, 1: TValue}>
1499
     * @return Standard<array-key, array{0: TKey, 1: TValue}>
1500
     */
1501
    public function tuples()
1502
    {
1503
        if ($this->empty()) {
73✔
1504
            // No-op: null.
1505
            return $this;
27✔
1506
        }
1507

1508
        if (is_array($this->pipeline)) {
71✔
1509
            $this->pipeline = array_map(
31✔
1510
                fn($key, $value) => [$key, $value],
31✔
1511
                array_keys($this->pipeline),
31✔
1512
                $this->pipeline
31✔
1513
            );
31✔
1514

1515
            return $this;
31✔
1516
        }
1517

1518

1519
        $this->pipeline = self::toTuples($this->pipeline);
65✔
1520

1521
        return $this;
65✔
1522
    }
1523

1524
    private static function toTuples(iterable $previous): Generator
1525
    {
1526
        foreach ($previous as $key => $value) {
69✔
1527
            yield [$key, $value];
55✔
1528
        }
1529
    }
1530

1531
    /** @return self<TKey, TValue> */
1532
    private function feedRunningVariance(Helper\RunningVariance $variance, ?callable $castFunc): self
1533
    {
1534
        if (null === $castFunc) {
9✔
1535
            $castFunc = floatval(...);
5✔
1536
        }
1537

1538
        return $this->cast(static function ($value) use ($variance, $castFunc) {
9✔
1539
            /** @var float|null $float */
1540
            $float = $castFunc($value);
9✔
1541

1542
            if (null !== $float) {
9✔
1543
                $variance->observe($float);
9✔
1544
            }
1545

1546
            // Returning the original value here
1547
            return $value;
9✔
1548
        });
9✔
1549
    }
1550

1551
    /**
1552
     * Feeds in an instance of RunningVariance.
1553
     *
1554
     * @param ?Helper\RunningVariance &$variance The instance of RunningVariance; initialized unless provided.
1555
     * @param ?callable $castFunc The cast callback, returning ?float; null values are not counted.
1556
     *
1557
     * @param-out Helper\RunningVariance $variance
1558
     *
1559
     * @phpstan-self-out self<TKey, TValue>
1560
     * @return Standard<TKey, TValue>
1561
     */
1562
    public function runningVariance(
1563
        ?Helper\RunningVariance &$variance,
1564
        ?callable $castFunc = null
1565
    ): self {
1566
        $variance ??= new Helper\RunningVariance();
4✔
1567

1568
        $this->feedRunningVariance($variance, $castFunc);
4✔
1569

1570
        return $this;
4✔
1571
    }
1572

1573
    /**
1574
     * Computes final statistics for the sequence.
1575
     *
1576
     * @param ?callable $castFunc The cast callback, returning ?float; null values are not counted.
1577
     * @param ?Helper\RunningVariance $variance The optional instance of RunningVariance.
1578
     */
1579
    public function finalVariance(
1580
        ?callable $castFunc = null,
1581
        ?Helper\RunningVariance $variance = null
1582
    ): Helper\RunningVariance {
1583
        $variance ??= new Helper\RunningVariance();
7✔
1584

1585
        if ($this->empty()) {
7✔
1586
            // No-op: an empty array.
1587
            return $variance;
2✔
1588
        }
1589

1590
        $this->feedRunningVariance($variance, $castFunc);
5✔
1591

1592
        if (is_array($this->pipeline)) {
5✔
1593
            // We are done!
1594
            return $variance;
2✔
1595
        }
1596

1597
        // Consume every available item (fastest way to do it)
1598
        $_ = iterator_count($this->pipeline);
3✔
1599

1600
        return $variance;
3✔
1601
    }
1602

1603
    /**
1604
     * Performs side effects on each element without changing the values in the pipeline.
1605
     *
1606
     * @param callable(TValue, TKey=): void $func A callback such as fn($value, $key); return value is ignored.
1607
     *
1608
     * @phpstan-self-out self<TKey, TValue>
1609
     * @return Standard<TKey, TValue>
1610
     */
1611
    public function tap(callable $func): self
1612
    {
1613
        if ($this->empty()) {
13✔
1614
            return $this;
2✔
1615
        }
1616

1617
        $this->pipeline = self::tapValues($this->pipeline, $func);
11✔
1618

1619
        return $this;
11✔
1620
    }
1621

1622
    private static function tapValues(iterable $previous, callable $func): Generator
1623
    {
1624
        foreach ($previous as $key => $value) {
10✔
1625
            self::callWithValueKey($func, $value, $key);
9✔
1626

1627
            yield $key => $value;
8✔
1628
        }
1629
    }
1630

1631
    /**
1632
     * Eagerly iterates over the sequence using the provided callback. Discards the sequence after iteration.
1633
     *
1634
     * @param callable(TValue, TKey=): void $func A callback such as fn($value, $key); return value is ignored.
1635
     * @param bool $discard Whether to discard the pipeline's iterator.
1636
     */
1637
    public function each(callable $func, bool $discard = true): void
1638
    {
1639
        try {
1640
            $this->eachInternal($func);
14✔
1641
        } finally {
1642
            if ($discard) {
14✔
1643
                $this->discard();
14✔
1644
            }
1645
        }
1646
    }
1647

1648
    /**
1649
     * @param callable(TValue, TKey=): void $func
1650
     */
1651
    private function eachInternal(callable $func): void
1652
    {
1653
        if ($this->empty()) {
14✔
1654
            return;
4✔
1655
        }
1656

1657
        foreach ($this->pipeline as $key => $value) {
12✔
1658
            self::callWithValueKey($func, $value, $key);
11✔
1659
        }
1660
    }
1661

1662
    /**
1663
     * Reference parameter allows wrapped callable to persist across iterations.
1664
     */
1665
    private static function callWithValueKey(callable &$func, mixed $value, mixed $key): void
1666
    {
1667
        try {
1668
            $func($value, $key);
26✔
1669
        } catch (ArgumentCountError) {
7✔
1670
            // Optimization to reduce the number of argument count errors when calling internal callables.
1671
            // This error is thrown when too many arguments are passed to a built-in function (that are sensitive
1672
            // to extra arguments), so we can wrap it to prevent the errors later. On the other hand, if there
1673
            // are too little arguments passed, it will blow up just a line later.
1674
            $func = self::wrapInternalCallable($func);
6✔
1675
            $func($value);
6✔
1676
        }
1677
    }
1678

1679
    /**
1680
     * Wraps internal callables to handle argument count mismatches by limiting to single argument.
1681
     */
1682
    private static function wrapInternalCallable(callable $func): callable
1683
    {
1684
        return static fn($value) => $func($value);
6✔
1685
    }
1686
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc