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

elephox-dev / framework / 4877852653

pending completion
4877852653

push

github

Ricardo Boss
WIP

38 of 38 new or added lines in 6 files covered. (100.0%)

3863 of 5835 relevant lines covered (66.2%)

8.55 hits per line

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

92.88
/modules/Collection/src/IsEnumerable.php
1
<?php
2
/** @noinspection PhpUnhandledExceptionInspection */
3
declare(strict_types=1);
4

5
namespace Elephox\Collection;
6

7
use AppendIterator;
8
use CachingIterator;
9
use CallbackFilterIterator;
10
use Countable;
11
use Elephox\Collection\Contract\GenericEnumerable;
12
use Elephox\Collection\Contract\GenericGroupedKeyedEnumerable;
13
use Elephox\Collection\Contract\GenericKeyedEnumerable;
14
use Elephox\Collection\Contract\GenericOrderedEnumerable;
15
use Elephox\Collection\Iterator\GroupingIterator;
16
use Elephox\Collection\Iterator\KeySelectIterator;
17
use Elephox\Collection\Iterator\OrderedIterator;
18
use Elephox\Collection\Iterator\ReverseIterator;
19
use Elephox\Collection\Iterator\SelectIterator;
20
use Elephox\Collection\Iterator\UniqueByIterator;
21
use Elephox\Collection\Iterator\WhileIterator;
22
use EmptyIterator;
23
use Generator;
24
use InvalidArgumentException;
25
use Iterator;
26
use IteratorIterator;
27
use JetBrains\PhpStorm\ExpectedValues;
28
use JsonException;
29
use LimitIterator;
30
use MultipleIterator as ParallelIterator;
31
use NoRewindIterator;
32
use RecursiveArrayIterator;
33
use RecursiveIteratorIterator;
34
use Traversable;
35

36
use const JSON_FORCE_OBJECT;
37
use const JSON_HEX_QUOT;
38
use const JSON_HEX_TAG;
39
use const JSON_HEX_AMP;
40
use const JSON_HEX_APOS;
41
use const JSON_INVALID_UTF8_IGNORE;
42
use const JSON_INVALID_UTF8_SUBSTITUTE;
43
use const JSON_NUMERIC_CHECK;
44
use const JSON_PARTIAL_OUTPUT_ON_ERROR;
45
use const JSON_PRESERVE_ZERO_FRACTION;
46
use const JSON_PRETTY_PRINT;
47
use const JSON_UNESCAPED_LINE_TERMINATORS;
48
use const JSON_UNESCAPED_SLASHES;
49
use const JSON_UNESCAPED_UNICODE;
50
use const JSON_THROW_ON_ERROR;
51

52
/**
53
 * @psalm-type NonNegativeInteger = int<0,max>
54
 *
55
 * @template TSource
56
 */
57
trait IsEnumerable
58
{
59
        // FIXME: de-duplicate code from IsEnumerable and IsKeyedEnumerable where possible (move iterator creation to trait and return self with created iterator)
60

61
        /**
62
         * @return Traversable<mixed, TSource>
63
         */
64
        abstract public function getIterator(): Traversable;
65

66
        /**
67
         * @template TAccumulate
68
         *
69
         * @param callable(TAccumulate|null, TSource): TAccumulate $accumulator
70
         * @param TAccumulate|null $seed
71
         *
72
         * @return TAccumulate
73
         */
74
        public function aggregate(callable $accumulator, mixed $seed = null): mixed
75
        {
76
                $result = $seed;
2✔
77

78
                foreach ($this->getIterator() as $element) {
2✔
79
                        $result = $accumulator($result, $element);
2✔
80
                }
81

82
                return $result;
2✔
83
        }
84

85
        public function all(callable $predicate): bool
86
        {
87
                foreach ($this->getIterator() as $element) {
1✔
88
                        if (!$predicate($element)) {
1✔
89
                                return false;
1✔
90
                        }
91
                }
92

93
                return true;
1✔
94
        }
95

96
        public function any(?callable $predicate = null): bool
97
        {
98
                foreach ($this->getIterator() as $element) {
1✔
99
                        if ($predicate === null || $predicate($element)) {
1✔
100
                                return true;
1✔
101
                        }
102
                }
103

104
                return false;
1✔
105
        }
106

107
        public function append(mixed $value): GenericEnumerable
108
        {
109
                return new Enumerable(function () use ($value) {
8✔
110
                        yield from $this->getIterator();
8✔
111

112
                        yield $value;
8✔
113
                });
8✔
114
        }
115

116
        public function appendAll(iterable $values): GenericEnumerable
117
        {
118
                return new Enumerable(function () use ($values) {
1✔
119
                        yield from $this->getIterator();
1✔
120
                        yield from $values;
1✔
121
                });
1✔
122
        }
123

124
        /**
125
         * @param callable(TSource): numeric $selector
126
         *
127
         * @return numeric
128
         */
129
        public function average(callable $selector): int|float|string
130
        {
131
                $sum = null;
2✔
132
                $count = 0;
2✔
133

134
                foreach ($this->getIterator() as $element) {
2✔
135
                        $value = $selector($element);
1✔
136

137
                        /** @var null|numeric $sum */
138
                        if ($sum === null) {
1✔
139
                                $sum = $value;
1✔
140
                        } else {
141
                                $sum += $value;
1✔
142
                        }
143

144
                        $count++;
1✔
145
                }
146

147
                if ($count === 0) {
2✔
148
                        throw new EmptySequenceException();
1✔
149
                }
150

151
                /** @var numeric $sum */
152
                return $sum / $count;
1✔
153
        }
154

155
        /**
156
         * @param NonNegativeInteger $size
157
         *
158
         * @return GenericEnumerable<non-empty-list<TSource>>
159
         */
160
        public function chunk(int $size): GenericEnumerable
161
        {
162
                if ($size <= 0) {
2✔
163
                        throw new InvalidArgumentException('Chunk size must be greater than zero.');
1✔
164
                }
165

166
                /** @var GenericEnumerable<non-empty-list<TSource>> */
167
                return new Enumerable(function () use ($size) {
1✔
168
                        $chunk = [];
1✔
169

170
                        foreach ($this->getIterator() as $element) {
1✔
171
                                if (count($chunk) === $size) {
1✔
172
                                        yield $chunk;
1✔
173

174
                                        $chunk = [$element];
1✔
175
                                } else {
176
                                        $chunk[] = $element;
1✔
177
                                }
178
                        }
179

180
                        if (!empty($chunk)) {
1✔
181
                                yield $chunk;
1✔
182
                        }
183
                });
1✔
184
        }
185

186
        public function concat(GenericEnumerable ...$other): GenericEnumerable
187
        {
188
                return new Enumerable(function () use ($other) {
1✔
189
                        yield from $this;
1✔
190

191
                        foreach ($other as $enumerable) {
1✔
192
                                yield from $enumerable;
1✔
193
                        }
194
                });
1✔
195
        }
196

197
        public function contains(mixed $value, ?callable $comparer = null): bool
198
        {
199
                $comparer ??= DefaultEqualityComparer::same(...);
21✔
200

201
                foreach ($this->getIterator() as $element) {
21✔
202
                        if ($comparer($value, $element)) {
6✔
203
                                return true;
4✔
204
                        }
205
                }
206

207
                return false;
21✔
208
        }
209

210
        /**
211
         * @param null|callable(TSource): bool $predicate
212
         *
213
         * @return NonNegativeInteger
214
         */
215
        public function count(?callable $predicate = null): int
216
        {
217
                $iterator = $this->getIterator();
14✔
218
                if ($predicate !== null) {
14✔
219
                        if (!($iterator instanceof Iterator)) {
1✔
220
                                $iterator = new IteratorIterator($iterator);
×
221
                        }
222
                        $iterator = new CallbackFilterIterator($iterator, $predicate);
1✔
223
                } elseif ($iterator instanceof Countable) {
14✔
224
                        /** @var NonNegativeInteger */
225
                        return $iterator->count();
10✔
226
                }
227

228
                return iterator_count($iterator);
8✔
229
        }
230

231
        /**
232
         * @param null|callable(TSource, TSource): bool $comparer
233
         *
234
         * @return GenericEnumerable<TSource>
235
         */
236
        public function distinct(?callable $comparer = null): GenericEnumerable
237
        {
238
                $comparer ??= DefaultEqualityComparer::same(...);
3✔
239
                $identity = static fn (mixed $element): mixed => $element;
3✔
240

241
                /**
242
                 * @var Closure(TSource, TSource): bool $comparer
243
                 * @var Closure(TSource): TSource $identity
244
                 */
245
                return $this->distinctBy($identity, $comparer);
3✔
246
        }
247

248
        /**
249
         * @template TCompareKey
250
         *
251
         * @param callable(TSource): TCompareKey $keySelector
252
         * @param null|callable(TCompareKey, TCompareKey): bool $comparer
253
         *
254
         * @return GenericEnumerable<TSource>
255
         */
256
        public function distinctBy(callable $keySelector, ?callable $comparer = null): GenericEnumerable
257
        {
258
                $comparer ??= DefaultEqualityComparer::same(...);
4✔
259

260
                $iterator = $this->getIterator();
4✔
261
                if (!($iterator instanceof Iterator)) {
4✔
262
                        $iterator = new IteratorIterator($iterator);
×
263
                }
264

265
                /**
266
                 * @var Closure(TSource, TSource): bool $comparer
267
                 * @var Closure(TSource): TSource $keySelector
268
                 */
269
                return new Enumerable(new UniqueByIterator($iterator, $keySelector(...), $comparer(...)));
4✔
270
        }
271

272
        /**
273
         * @param GenericEnumerable<TSource> $other
274
         * @param null|callable(TSource, TSource): bool $comparer
275
         *
276
         * @return GenericEnumerable<TSource>
277
         */
278
        public function except(GenericEnumerable $other, ?callable $comparer = null): GenericEnumerable
279
        {
280
                $comparer ??= DefaultEqualityComparer::same(...);
1✔
281

282
                return $this->exceptBy($other, static fn (mixed $element): mixed => $element, $comparer);
1✔
283
        }
284

285
        /**
286
         * @template TCompareKey
287
         *
288
         * @param GenericEnumerable<TSource> $other
289
         * @param callable(TSource): TCompareKey $keySelector
290
         * @param null|callable(TCompareKey, TCompareKey): bool $comparer
291
         *
292
         * @return GenericEnumerable<TSource>
293
         */
294
        public function exceptBy(GenericEnumerable $other, callable $keySelector, ?callable $comparer = null): GenericEnumerable
295
        {
296
                $comparer ??= DefaultEqualityComparer::same(...);
2✔
297

298
                /** @var Iterator<mixed, TSource> $otherIterator */
299
                $otherIterator = $other->getIterator();
2✔
300

301
                return new Enumerable(function () use ($otherIterator, $keySelector, $comparer) {
2✔
302
                        /** @var Iterator<mixed, TCompareKey> $otherKeys */
303
                        $otherKeys = new CachingIterator(new SelectIterator($otherIterator, $keySelector(...)), CachingIterator::FULL_CACHE);
2✔
304

305
                        foreach ($this->getIterator() as $element) {
2✔
306
                                $key = $keySelector($element);
2✔
307

308
                                foreach ($otherKeys as $otherKey) {
2✔
309
                                        if ($comparer($key, $otherKey)) {
2✔
310
                                                continue 2;
2✔
311
                                        }
312
                                }
313

314
                                yield $element;
2✔
315
                        }
316
                });
2✔
317
        }
318

319
        public function first(?callable $predicate = null): mixed
320
        {
321
                foreach ($this->getIterator() as $element) {
5✔
322
                        if ($predicate === null || $predicate($element)) {
5✔
323
                                return $element;
5✔
324
                        }
325
                }
326

327
                throw new EmptySequenceException();
1✔
328
        }
329

330
        public function firstOrDefault(mixed $defaultValue, ?callable $predicate = null): mixed
331
        {
332
                foreach ($this->getIterator() as $element) {
1✔
333
                        if ($predicate === null || $predicate($element)) {
1✔
334
                                return $element;
1✔
335
                        }
336
                }
337

338
                return $defaultValue;
1✔
339
        }
340

341
        /**
342
         * @return GenericEnumerable<TSource>
343
         */
344
        public function flatten(): GenericEnumerable
345
        {
346
                return new Enumerable(function () {
1✔
347
                        $it = new RecursiveIteratorIterator(new RecursiveArrayIterator($this->getIterator()));
1✔
348
                        foreach ($it as $v) {
1✔
349
                                yield $v;
1✔
350
                        }
351
                });
1✔
352
        }
353

354
        /**
355
         * @param callable(TSource): void $callback
356
         *
357
         * @return void
358
         */
359
        public function forEach(callable $callback): void
360
        {
361
                foreach ($this->getIterator() as $element) {
×
362
                        $callback($element);
×
363
                }
364
        }
365

366
        /**
367
         * @template TGroupKey
368
         *
369
         * @param callable(TSource): TGroupKey $keySelector
370
         * @param null|callable(TSource, TSource): bool $comparer
371
         *
372
         * @return GenericGroupedKeyedEnumerable<TGroupKey, mixed, TSource>
373
         */
374
        public function groupBy(callable $keySelector, ?callable $comparer = null): GenericGroupedKeyedEnumerable
375
        {
376
                $comparer ??= DefaultEqualityComparer::same(...);
1✔
377

378
                return new GroupedKeyedEnumerable(new GroupingIterator($this->getIterator(), $keySelector(...), $comparer(...)));
1✔
379
        }
380

381
        public function implode(string $separator = ', ', ?callable $toString = null): string
382
        {
383
                $toString ??= static fn (mixed $v): string => (string) $v;
1✔
384
                $strings = [];
1✔
385

386
                foreach ($this->getIterator() as $value) {
1✔
387
                        $strings[] = $toString($value);
1✔
388
                }
389

390
                return implode($separator, $strings);
1✔
391
        }
392

393
        /**
394
         * @param GenericEnumerable<TSource> $other
395
         * @param null|callable(TSource, TSource): bool $comparer
396
         *
397
         * @return GenericEnumerable<TSource>
398
         */
399
        public function intersect(GenericEnumerable $other, ?callable $comparer = null): GenericEnumerable
400
        {
401
                $comparer ??= DefaultEqualityComparer::same(...);
1✔
402

403
                return $this->intersectBy($other, static fn ($element): mixed => $element, $comparer);
1✔
404
        }
405

406
        /**
407
         * @template TKey
408
         *
409
         * @param GenericEnumerable<TSource> $other
410
         * @param callable(TSource): TKey $keySelector
411
         * @param null|callable(TSource, TSource): bool $comparer
412
         *
413
         * @return GenericEnumerable<TSource>
414
         */
415
        public function intersectBy(GenericEnumerable $other, callable $keySelector, ?callable $comparer = null): GenericEnumerable
416
        {
417
                $comparer ??= DefaultEqualityComparer::same(...);
2✔
418

419
                return new Enumerable(function () use ($other, $keySelector, $comparer) {
2✔
420
                        $otherKeys = [];
2✔
421
                        foreach ($other->getIterator() as $otherElement) {
2✔
422
                                $otherKeys[] = $keySelector($otherElement);
2✔
423
                        }
424

425
                        foreach ($this->getIterator() as $element) {
2✔
426
                                $key = $keySelector($element);
2✔
427

428
                                foreach ($otherKeys as $otherKey) {
2✔
429
                                        if ($comparer($key, $otherKey)) {
2✔
430
                                                yield $element;
2✔
431

432
                                                continue 2;
2✔
433
                                        }
434
                                }
435
                        }
436
                });
2✔
437
        }
438

439
        public function isEmpty(): bool
440
        {
441
                return $this->count() === 0;
9✔
442
        }
443

444
        public function isNotEmpty(): bool
445
        {
446
                return !$this->isEmpty();
1✔
447
        }
448

449
        /**
450
         * @template TInner
451
         * @template TCompareKey
452
         * @template TResult
453
         *
454
         * @param GenericEnumerable<TInner> $inner
455
         * @param callable(TSource): TCompareKey $outerKeySelector
456
         * @param callable(TInner): TCompareKey $innerKeySelector
457
         * @param callable(TSource, TInner): TResult $resultSelector
458
         * @param null|callable(TCompareKey, TCompareKey): bool $comparer
459
         *
460
         * @return GenericEnumerable<TSource>
461
         */
462
        public function join(GenericEnumerable $inner, callable $outerKeySelector, callable $innerKeySelector, callable $resultSelector, ?callable $comparer = null): GenericEnumerable
463
        {
464
                $comparer ??= DefaultEqualityComparer::same(...);
1✔
465

466
                return new Enumerable(function () use ($inner, $outerKeySelector, $innerKeySelector, $resultSelector, $comparer): Generator {
1✔
467
                        $innerKeys = [];
1✔
468
                        $innerElements = [];
1✔
469
                        foreach ($inner->getIterator() as $innerElement) {
1✔
470
                                $innerKeys[] = $innerKeySelector($innerElement);
1✔
471
                                $innerElements[] = $innerElement;
1✔
472
                        }
473

474
                        foreach ($this->getIterator() as $outerElement) {
1✔
475
                                $outerKey = $outerKeySelector($outerElement);
1✔
476

477
                                foreach ($innerKeys as $index => $innerKey) {
1✔
478
                                        if ($comparer($outerKey, $innerKey)) {
1✔
479
                                                yield $resultSelector($outerElement, $innerElements[$index]);
1✔
480
                                        }
481
                                }
482
                        }
483
                });
1✔
484
        }
485

486
        public function last(?callable $predicate = null): mixed
487
        {
488
                $last = null;
2✔
489
                foreach ($this->getIterator() as $element) {
2✔
490
                        if ($predicate === null || $predicate($element)) {
1✔
491
                                $last = $element;
1✔
492
                        }
493
                }
494

495
                if ($last === null) {
2✔
496
                        throw new EmptySequenceException();
1✔
497
                }
498

499
                return $last;
1✔
500
        }
501

502
        public function lastOrDefault(mixed $default, ?callable $predicate = null): mixed
503
        {
504
                $last = null;
1✔
505
                foreach ($this->getIterator() as $element) {
1✔
506
                        if ($predicate === null || $predicate($element)) {
1✔
507
                                $last = $element;
1✔
508
                        }
509
                }
510

511
                return $last ?? $default;
1✔
512
        }
513

514
        /**
515
         * @param callable(TSource): numeric $selector
516
         *
517
         * @return numeric
518
         */
519
        public function max(callable $selector): int|float|string
520
        {
521
                $iterator = $this->getIterator();
2✔
522
                if (!($iterator instanceof Iterator)) {
2✔
523
                        $iterator = new IteratorIterator($iterator);
×
524
                }
525
                $iterator->rewind();
2✔
526
                if (!$iterator->valid()) {
2✔
527
                        throw new EmptySequenceException();
1✔
528
                }
529

530
                $max = $selector($iterator->current());
1✔
531
                $iterator->next();
1✔
532

533
                while ($iterator->valid()) {
1✔
534
                        $max = max($max, $selector($iterator->current()));
1✔
535

536
                        $iterator->next();
1✔
537
                }
538

539
                return $max;
1✔
540
        }
541

542
        /**
543
         * @param callable(TSource): numeric $selector
544
         *
545
         * @return numeric
546
         */
547
        public function min(callable $selector): int|float|string
548
        {
549
                $iterator = $this->getIterator();
2✔
550
                if (!($iterator instanceof Iterator)) {
2✔
551
                        $iterator = new IteratorIterator($iterator);
×
552
                }
553
                $iterator->rewind();
2✔
554
                if (!$iterator->valid()) {
2✔
555
                        throw new EmptySequenceException();
1✔
556
                }
557

558
                $min = $selector($iterator->current());
1✔
559
                $iterator->next();
1✔
560

561
                while ($iterator->valid()) {
1✔
562
                        $min = min($min, $selector($iterator->current()));
1✔
563

564
                        $iterator->next();
1✔
565
                }
566

567
                return $min;
1✔
568
        }
569

570
        /**
571
         * @template TCompareKey
572
         *
573
         * @param callable(TSource): TCompareKey $keySelector
574
         * @param null|callable(TCompareKey, TCompareKey): int $comparer
575
         *
576
         * @return GenericOrderedEnumerable<TSource>
577
         */
578
        public function orderBy(callable $keySelector, ?callable $comparer = null): GenericOrderedEnumerable
579
        {
580
                $comparer ??= DefaultEqualityComparer::compare(...);
2✔
581

582
                return new OrderedEnumerable(new OrderedIterator($this->getIterator(), $keySelector(...), $comparer(...)));
2✔
583
        }
584

585
        /**
586
         * @template TCompareKey
587
         *
588
         * @param callable(TSource): TCompareKey $keySelector
589
         * @param null|callable(TCompareKey, TCompareKey): int $comparer
590
         *
591
         * @return GenericOrderedEnumerable<TSource>
592
         */
593
        public function orderByDescending(callable $keySelector, ?callable $comparer = null): GenericOrderedEnumerable
594
        {
595
                $comparer ??= DefaultEqualityComparer::compare(...);
6✔
596
                $comparer = DefaultEqualityComparer::invert($comparer);
6✔
597
                /** @var callable(mixed, mixed): int $comparer */
598

599
                return new OrderedEnumerable(new OrderedIterator($this->getIterator(), $keySelector(...), $comparer(...)));
6✔
600
        }
601

602
        public function prepend(mixed $value): GenericEnumerable
603
        {
604
                return new Enumerable(function () use ($value) {
1✔
605
                        yield $value;
1✔
606

607
                        yield from $this->getIterator();
1✔
608
                });
1✔
609
        }
610

611
        public function prependAll(iterable $values): GenericEnumerable
612
        {
613
                return new Enumerable(function () use ($values) {
1✔
614
                        yield from $values;
1✔
615

616
                        yield from $this->getIterator();
1✔
617
                });
1✔
618
        }
619

620
        public function reverse(bool $preserveKeys = true): GenericEnumerable
621
        {
622
                return new Enumerable(new ReverseIterator($this->getIterator(), $preserveKeys));
8✔
623
        }
624

625
        /**
626
         * @template TResult
627
         *
628
         * @param callable(TSource): TResult $selector
629
         *
630
         * @return GenericEnumerable<TResult>
631
         */
632
        public function select(callable $selector): GenericEnumerable
633
        {
634
                $iterator = $this->getIterator();
3✔
635
                if (!($iterator instanceof Iterator)) {
3✔
636
                        $iterator = new IteratorIterator($iterator);
×
637
                }
638

639
                return new Enumerable(new SelectIterator($iterator, $selector(...)));
3✔
640
        }
641

642
        /**
643
         * @template TCollection
644
         * @template TIntermediateKey
645
         * @template TCollectionKey
646
         * @template TResult
647
         *
648
         * @param callable(TSource): GenericKeyedEnumerable<TIntermediateKey, TCollection> $collectionSelector
649
         * @param null|callable(TSource, TCollection, TIntermediateKey): TResult $resultSelector
650
         * @param null|callable(TSource, TCollection, TIntermediateKey): TCollectionKey $keySelector
651
         *
652
         * @return GenericKeyedEnumerable<TCollectionKey, TResult>
653
         */
654
        public function selectManyKeyed(callable $collectionSelector, ?callable $resultSelector = null, ?callable $keySelector = null): GenericKeyedEnumerable
655
        {
656
                $resultSelector ??= static fn (mixed $element, mixed $collectionElement, mixed $collectionElementKey): mixed => $collectionElement;
1✔
657
                $keySelector ??= static fn (mixed $element, mixed $collectionElement, mixed $collectionElementKey): mixed => $collectionElementKey;
1✔
658
                /** @var callable(TSource, TCollection, TIntermediateKey): TCollectionKey $keySelector */
659

660
                return new KeyedEnumerable(function () use ($collectionSelector, $resultSelector, $keySelector) {
1✔
661
                        foreach ($this->getIterator() as $element) {
1✔
662
                                foreach ($collectionSelector($element) as $collectionElementKey => $collectionElement) {
1✔
663
                                        yield $keySelector($element, $collectionElement, $collectionElementKey) => $resultSelector($element, $collectionElement, $collectionElementKey);
1✔
664
                                }
665
                        }
666
                });
1✔
667
        }
668

669
        /**
670
         * @template TCollection
671
         * @template TResult
672
         *
673
         * @param callable(TSource): GenericEnumerable<TCollection> $collectionSelector
674
         * @param null|callable(TSource, TCollection): TResult $resultSelector
675
         *
676
         * @return GenericEnumerable<TResult>
677
         */
678
        public function selectMany(callable $collectionSelector, ?callable $resultSelector = null): GenericEnumerable
679
        {
680
                $resultSelector ??= static fn (mixed $element, mixed $collectionElement): mixed => $collectionElement;
2✔
681
                /** @var callable(TSource, TCollection): TResult $resultSelector */
682

683
                return new Enumerable(function () use ($collectionSelector, $resultSelector) {
2✔
684
                        /** @var TSource $element */
685
                        foreach ($this->getIterator() as $element) {
2✔
686
                                foreach ($collectionSelector($element) as $collectionElement) {
2✔
687
                                        yield $resultSelector($element, $collectionElement);
1✔
688
                                }
689
                        }
690
                });
2✔
691
        }
692

693
        public function sequenceEqual(GenericEnumerable $other, ?callable $comparer = null): bool
694
        {
695
                $comparer ??= DefaultEqualityComparer::same(...);
1✔
696
                /** @var callable(TSource, TSource): bool $comparer */
697
                $otherIterator = $other->getIterator();
1✔
698
                if (!($otherIterator instanceof Iterator)) {
1✔
699
                        $otherIterator = new IteratorIterator($otherIterator);
×
700
                }
701
                /** @var Iterator<mixed, TSource> $otherIterator */
702
                $iterator = $this->getIterator();
1✔
703
                if (!($iterator instanceof Iterator)) {
1✔
704
                        $iterator = new IteratorIterator($iterator);
×
705
                }
706

707
                $mit = new ParallelIterator(ParallelIterator::MIT_KEYS_NUMERIC | ParallelIterator::MIT_NEED_ANY);
1✔
708
                $mit->attachIterator($iterator);
1✔
709
                $mit->attachIterator($otherIterator);
1✔
710

711
                foreach ($mit as $values) {
1✔
712
                        /**
713
                         * @var array{TSource, TSource} $values
714
                         */
715
                        if (!$comparer($values[0], $values[1])) {
1✔
716
                                return false;
1✔
717
                        }
718
                }
719

720
                return true;
1✔
721
        }
722

723
        public function single(?callable $predicate = null): mixed
724
        {
725
                $matched = false;
3✔
726
                $returnElement = null;
3✔
727

728
                foreach ($this->getIterator() as $element) {
3✔
729
                        if ($predicate === null || $predicate($element)) {
2✔
730
                                if ($matched) {
2✔
731
                                        throw new AmbiguousMatchException();
1✔
732
                                }
733

734
                                $matched = true;
2✔
735
                                $returnElement = $element;
2✔
736
                        }
737
                }
738

739
                if (!$matched) {
2✔
740
                        throw new EmptySequenceException();
1✔
741
                }
742

743
                return $returnElement;
1✔
744
        }
745

746
        public function singleOrDefault(mixed $default, ?callable $predicate = null): mixed
747
        {
748
                $matched = false;
2✔
749
                $returnElement = null;
2✔
750

751
                foreach ($this->getIterator() as $element) {
2✔
752
                        if ($predicate === null || $predicate($element)) {
2✔
753
                                if ($matched) {
2✔
754
                                        throw new AmbiguousMatchException();
1✔
755
                                }
756

757
                                $matched = true;
2✔
758
                                $returnElement = $element;
2✔
759
                        }
760
                }
761

762
                return $matched ? $returnElement : $default;
1✔
763
        }
764

765
        public function skip(int $count): GenericEnumerable
766
        {
767
                $iterator = $this->getIterator();
2✔
768
                if (!($iterator instanceof Iterator)) {
2✔
769
                        $iterator = new IteratorIterator($iterator);
×
770
                }
771

772
                return new Enumerable(new LimitIterator($iterator, $count));
2✔
773
        }
774

775
        public function skipLast(int $count): GenericEnumerable
776
        {
777
                if ($count <= 0) {
2✔
778
                        throw new InvalidArgumentException('Count must be greater than zero');
1✔
779
                }
780

781
                $iterator = $this->getIterator();
2✔
782
                if (!($iterator instanceof Iterator)) {
2✔
783
                        $iterator = new IteratorIterator($iterator);
×
784
                }
785
                $cachedIterator = new CachingIterator($iterator, CachingIterator::FULL_CACHE);
2✔
786
                $cachedIterator->rewind();
2✔
787
                while ($cachedIterator->valid()) {
2✔
788
                        $cachedIterator->next();
1✔
789
                }
790

791
                $size = count($cachedIterator);
2✔
792
                $offset = $size - $count;
2✔
793
                if ($offset > 0) {
2✔
794
                        $iterator = new LimitIterator($cachedIterator, 0, $offset);
1✔
795
                } else {
796
                        $iterator = new EmptyIterator();
2✔
797
                }
798

799
                return new Enumerable($iterator);
2✔
800
        }
801

802
        /**
803
         * @param callable(TSource): bool $predicate
804
         *
805
         * @return GenericEnumerable<TSource>
806
         */
807
        public function skipWhile(callable $predicate): GenericEnumerable
808
        {
809
                $iterator = $this->getIterator();
1✔
810
                if (!($iterator instanceof Iterator)) {
1✔
811
                        $iterator = new IteratorIterator($iterator);
×
812
                }
813

814
                $whileIterator = new WhileIterator($iterator, $predicate(...));
1✔
815
                $whileIterator->rewind();
1✔
816
                while ($whileIterator->valid()) {
1✔
817
                        $whileIterator->next();
1✔
818
                }
819

820
                return new Enumerable(new NoRewindIterator($iterator));
1✔
821
        }
822

823
        /**
824
         * @param callable(TSource): numeric $selector
825
         *
826
         * @return numeric
827
         */
828
        public function sum(callable $selector): int|float|string
829
        {
830
                /** @var numeric */
831
                return $this->aggregate(static function (mixed $accumulator, mixed $element) use ($selector) {
1✔
832
                        /**
833
                         * @var numeric $accumulator
834
                         * @var TSource $element
835
                         */
836
                        return $accumulator + $selector($element);
1✔
837
                }, 0);
1✔
838
        }
839

840
        public function take(int $count): GenericEnumerable
841
        {
842
                $iterator = $this->getIterator();
1✔
843
                if (!($iterator instanceof Iterator)) {
1✔
844
                        $iterator = new IteratorIterator($iterator);
×
845
                }
846

847
                return new Enumerable(new LimitIterator($iterator, 0, $count));
1✔
848
        }
849

850
        public function takeLast(int $count): GenericEnumerable
851
        {
852
                $iterator = $this->getIterator();
3✔
853
                if (!($iterator instanceof Iterator)) {
3✔
854
                        $iterator = new IteratorIterator($iterator);
×
855
                }
856
                $cachedIterator = new CachingIterator($iterator, CachingIterator::FULL_CACHE);
3✔
857
                $cachedIterator->rewind();
3✔
858
                while ($cachedIterator->valid()) {
3✔
859
                        $cachedIterator->next();
2✔
860
                }
861

862
                $size = count($cachedIterator);
3✔
863
                $offset = $size - $count;
3✔
864
                if ($offset < 0) {
3✔
865
                        return new Enumerable(new EmptyIterator());
1✔
866
                }
867

868
                return new Enumerable(new LimitIterator($cachedIterator, $offset));
2✔
869
        }
870

871
        /**
872
         * @param callable(TSource): bool $predicate
873
         *
874
         * @return GenericEnumerable<TSource>
875
         */
876
        public function takeWhile(callable $predicate): GenericEnumerable
877
        {
878
                $iterator = $this->getIterator();
1✔
879
                if (!($iterator instanceof Iterator)) {
1✔
880
                        $iterator = new IteratorIterator($iterator);
×
881
                }
882

883
                return new Enumerable(new WhileIterator($iterator, $predicate(...)));
1✔
884
        }
885

886
        /**
887
         * @return list<TSource>
888
         */
889
        public function toList(): array
890
        {
891
                $list = [];
39✔
892

893
                foreach ($this->getIterator() as $element) {
39✔
894
                        $list[] = $element;
36✔
895
                }
896

897
                return $list;
39✔
898
        }
899

900
        /**
901
         * @return ArrayList<TSource>
902
         */
903
        public function toArrayList(): ArrayList
904
        {
905
                return new ArrayList($this->toList());
7✔
906
        }
907

908
        /**
909
         * @throws JsonException
910
         */
911
        public function toJson(
912
                #[ExpectedValues(flags: [
913
                        JSON_FORCE_OBJECT,
914
                        JSON_HEX_QUOT,
915
                        JSON_HEX_TAG,
916
                        JSON_HEX_AMP,
917
                        JSON_HEX_APOS,
918
                        JSON_INVALID_UTF8_IGNORE,
919
                        JSON_INVALID_UTF8_SUBSTITUTE,
920
                        JSON_NUMERIC_CHECK,
921
                        JSON_PARTIAL_OUTPUT_ON_ERROR,
922
                        JSON_PRESERVE_ZERO_FRACTION,
923
                        JSON_PRETTY_PRINT,
924
                        JSON_UNESCAPED_LINE_TERMINATORS,
925
                        JSON_UNESCAPED_SLASHES,
926
                        JSON_UNESCAPED_UNICODE,
927
                        JSON_THROW_ON_ERROR,
928
                ])] int $flags = 0,
929
                int $depth = 512,
930
        ): string {
931
                return json_encode($this->toList(), $flags | JSON_THROW_ON_ERROR, $depth);
×
932
        }
933

934
        /**
935
         * @template USource
936
         * @template UKey
937
         *
938
         * @param Iterator<UKey, USource> $iterator
939
         *
940
         * @return GenericKeyedEnumerable<NonNegativeInteger, USource>
941
         */
942
        private static function reindex(Iterator $iterator): GenericKeyedEnumerable
943
        {
944
                $key = 0;
12✔
945

946
                return new KeyedEnumerable(new KeySelectIterator($iterator, static function () use (&$key): int {
12✔
947
                        /**
948
                         * @var NonNegativeInteger $key
949
                         */
950
                        return $key++;
12✔
951
                }));
12✔
952
        }
953

954
        /**
955
         * @template TArrayKey as array-key
956
         *
957
         * @param null|callable(NonNegativeInteger, TSource): TArrayKey $keySelector
958
         *
959
         * @return array<TArrayKey, TSource>
960
         */
961
        public function toArray(?callable $keySelector = null): array
962
        {
963
                $iterator = $this->getIterator();
11✔
964
                if (!($iterator instanceof Iterator)) {
11✔
965
                        $iterator = new IteratorIterator($iterator);
×
966
                }
967

968
                return self::reindex($iterator)->toArray($keySelector);
11✔
969
        }
970

971
        /**
972
         * @template TArrayKey as array-key
973
         *
974
         * @param null|callable(NonNegativeInteger, TSource): TArrayKey $keySelector
975
         *
976
         * @return array<TArrayKey, list<TSource>>
977
         */
978
        public function toNestedArray(?callable $keySelector = null): array
979
        {
980
                $iterator = $this->getIterator();
1✔
981
                if (!($iterator instanceof Iterator)) {
1✔
982
                        $iterator = new IteratorIterator($iterator);
×
983
                }
984

985
                return self::reindex($iterator)->groupByKey($keySelector);
1✔
986
        }
987

988
        public function toKeyed(callable $keySelector): GenericKeyedEnumerable
989
        {
990
                $valueProxy = static fn (mixed $key, mixed $value): mixed => $keySelector($value);
1✔
991

992
                $iterator = $this->getIterator();
1✔
993
                if (!($iterator instanceof Iterator)) {
1✔
994
                        $iterator = new IteratorIterator($iterator);
×
995
                }
996

997
                return new KeyedEnumerable(new KeySelectIterator($iterator, $valueProxy(...)));
1✔
998
        }
999

1000
        /**
1001
         * @param GenericEnumerable<TSource> $other
1002
         * @param null|callable(TSource, TSource): bool $comparer
1003
         *
1004
         * @return GenericEnumerable<TSource>
1005
         */
1006
        public function union(GenericEnumerable $other, ?callable $comparer = null): GenericEnumerable
1007
        {
1008
                $comparer ??= DefaultEqualityComparer::same(...);
1✔
1009
                $identity = static fn (mixed $o): mixed => $o;
1✔
1010

1011
                /**
1012
                 * @var callable(TSource, TSource): bool $comparer
1013
                 * @var callable(TSource): TSource $identity
1014
                 */
1015
                return $this->unionBy($other, $identity, $comparer);
1✔
1016
        }
1017

1018
        /**
1019
         * @template TCompareKey
1020
         *
1021
         * @param GenericEnumerable<TSource> $other
1022
         * @param callable(TSource): TCompareKey $keySelector
1023
         * @param null|callable(TCompareKey, TCompareKey): bool $comparer
1024
         *
1025
         * @return GenericEnumerable<TSource>
1026
         */
1027
        public function unionBy(GenericEnumerable $other, callable $keySelector, ?callable $comparer = null): GenericEnumerable
1028
        {
1029
                $comparer ??= DefaultEqualityComparer::same(...);
2✔
1030

1031
                $otherIterator = $other->getIterator();
2✔
1032
                if (!($otherIterator instanceof Iterator)) {
2✔
1033
                        $otherIterator = new IteratorIterator($otherIterator);
×
1034
                }
1035

1036
                $iterator = $this->getIterator();
2✔
1037
                if (!($iterator instanceof Iterator)) {
2✔
1038
                        $iterator = new IteratorIterator($iterator);
×
1039
                }
1040

1041
                $append = new AppendIterator();
2✔
1042
                $append->append($iterator);
2✔
1043
                $append->append($otherIterator);
2✔
1044

1045
                /**
1046
                 * @var Closure(TSource): TCompareKey $keySelector
1047
                 * @var Closure(TCompareKey, TCompareKey): bool $comparer
1048
                 */
1049
                return new Enumerable(new UniqueByIterator($append, $keySelector(...), $comparer(...)));
2✔
1050
        }
1051

1052
        /**
1053
         * @param null|callable(TSource, TSource): bool $comparer
1054
         *
1055
         * @return GenericEnumerable<TSource>
1056
         */
1057
        public function unique(?callable $comparer = null): GenericEnumerable
1058
        {
1059
                $comparer ??= DefaultEqualityComparer::same(...);
7✔
1060
                $identity = static fn (mixed $o): mixed => $o;
7✔
1061

1062
                /**
1063
                 * @var callable(TSource, TSource): bool $comparer
1064
                 * @var callable(TSource): TSource $identity
1065
                 */
1066
                return $this->uniqueBy($identity, $comparer);
7✔
1067
        }
1068

1069
        /**
1070
         * @template TCompareKey
1071
         *
1072
         * @param callable(TSource): TCompareKey $keySelector
1073
         * @param null|callable(TCompareKey, TCompareKey): bool $comparer
1074
         *
1075
         * @return GenericEnumerable<TSource>
1076
         */
1077
        public function uniqueBy(callable $keySelector, ?callable $comparer = null): GenericEnumerable
1078
        {
1079
                $comparer ??= DefaultEqualityComparer::same(...);
7✔
1080

1081
                $iterator = $this->getIterator();
7✔
1082
                if (!($iterator instanceof Iterator)) {
7✔
1083
                        $iterator = new IteratorIterator($iterator);
×
1084
                }
1085

1086
                /**
1087
                 * @var Closure(TSource): TCompareKey $keySelector
1088
                 * @var Closure(TCompareKey, TCompareKey): bool $comparer
1089
                 */
1090
                return new Enumerable(new UniqueByIterator($iterator, $keySelector(...), $comparer(...)));
7✔
1091
        }
1092

1093
        /**
1094
         * @param callable(TSource): bool $predicate
1095
         *
1096
         * @return GenericEnumerable<TSource>
1097
         */
1098
        public function where(callable $predicate): GenericEnumerable
1099
        {
1100
                $iterator = $this->getIterator();
4✔
1101
                if (!($iterator instanceof Iterator)) {
4✔
1102
                        $iterator = new IteratorIterator($iterator);
×
1103
                }
1104

1105
                /** @var Iterator<mixed, TSource> $iterator */
1106
                return new Enumerable(new CallbackFilterIterator($iterator, $predicate(...)));
4✔
1107
        }
1108

1109
        /**
1110
         * @template TOther
1111
         * @template TResult
1112
         *
1113
         * @param GenericEnumerable<TOther> $other
1114
         * @param null|callable(TSource, TOther): TResult $resultSelector
1115
         *
1116
         * @return GenericEnumerable<TResult>
1117
         */
1118
        public function zip(GenericEnumerable $other, ?callable $resultSelector = null): GenericEnumerable
1119
        {
1120
                $resultSelector ??= static fn (mixed $a, mixed $b): array => [$a, $b];
1✔
1121

1122
                $otherIterator = $other->getIterator();
1✔
1123
                if (!($otherIterator instanceof Iterator)) {
1✔
1124
                        $otherIterator = new IteratorIterator($otherIterator);
×
1125
                }
1126

1127
                $iterator = $this->getIterator();
1✔
1128
                if (!($iterator instanceof Iterator)) {
1✔
1129
                        $iterator = new IteratorIterator($iterator);
×
1130
                }
1131

1132
                $mit = new ParallelIterator(ParallelIterator::MIT_KEYS_NUMERIC | ParallelIterator::MIT_NEED_ALL);
1✔
1133
                $mit->attachIterator($iterator);
1✔
1134
                $mit->attachIterator($otherIterator);
1✔
1135
                /** @var ParallelIterator $mit */
1136

1137
                /** @var GenericEnumerable<TResult> */
1138
                return new Enumerable(
1✔
1139
                        /** @var SelectIterator<mixed, TResult> */
1140
                        new SelectIterator(
1✔
1141
                                $mit,
1✔
1142
                                static function (mixed $values) use ($resultSelector): array {
1✔
1143
                                        /** @var array{TSource, TOther} $values */
1144
                                        return $resultSelector($values[0], $values[1]);
1✔
1145
                                },
1✔
1146
                        ),
1✔
1147
                );
1✔
1148
        }
1149
}
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

© 2025 Coveralls, Inc