blob: 464cee1b88cef31aa987bfa1c5e81e427066ce0f [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace Tightenco\Collect\Support;
4
5use ArrayIterator;
6use Closure;
7use DateTimeInterface;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01008use Tightenco\Collect\Contracts\Support\CanBeEscapedWhenCastToString;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02009use Tightenco\Collect\Support\Traits\EnumeratesValues;
10use Tightenco\Collect\Support\Traits\Macroable;
11use IteratorAggregate;
12use stdClass;
13
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010014class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020015{
16 use EnumeratesValues, Macroable;
17
18 /**
19 * The source from which to generate items.
20 *
21 * @var callable|static
22 */
23 public $source;
24
25 /**
26 * Create a new lazy collection instance.
27 *
28 * @param mixed $source
29 * @return void
30 */
31 public function __construct($source = null)
32 {
33 if ($source instanceof Closure || $source instanceof self) {
34 $this->source = $source;
35 } elseif (is_null($source)) {
36 $this->source = static::empty();
37 } else {
38 $this->source = $this->getArrayableItems($source);
39 }
40 }
41
42 /**
43 * Create a collection with the given range.
44 *
45 * @param int $from
46 * @param int $to
47 * @return static
48 */
49 public static function range($from, $to)
50 {
51 return new static(function () use ($from, $to) {
52 if ($from <= $to) {
53 for (; $from <= $to; $from++) {
54 yield $from;
55 }
56 } else {
57 for (; $from >= $to; $from--) {
58 yield $from;
59 }
60 }
61 });
62 }
63
64 /**
65 * Get all items in the enumerable.
66 *
67 * @return array
68 */
69 public function all()
70 {
71 if (is_array($this->source)) {
72 return $this->source;
73 }
74
75 return iterator_to_array($this->getIterator());
76 }
77
78 /**
79 * Eager load all items into a new lazy collection backed by an array.
80 *
81 * @return static
82 */
83 public function eager()
84 {
85 return new static($this->all());
86 }
87
88 /**
89 * Cache values as they're enumerated.
90 *
91 * @return static
92 */
93 public function remember()
94 {
95 $iterator = $this->getIterator();
96
97 $iteratorIndex = 0;
98
99 $cache = [];
100
101 return new static(function () use ($iterator, &$iteratorIndex, &$cache) {
102 for ($index = 0; true; $index++) {
103 if (array_key_exists($index, $cache)) {
104 yield $cache[$index][0] => $cache[$index][1];
105
106 continue;
107 }
108
109 if ($iteratorIndex < $index) {
110 $iterator->next();
111
112 $iteratorIndex++;
113 }
114
115 if (! $iterator->valid()) {
116 break;
117 }
118
119 $cache[$index] = [$iterator->key(), $iterator->current()];
120
121 yield $cache[$index][0] => $cache[$index][1];
122 }
123 });
124 }
125
126 /**
127 * Get the average value of a given key.
128 *
129 * @param callable|string|null $callback
130 * @return mixed
131 */
132 public function avg($callback = null)
133 {
134 return $this->collect()->avg($callback);
135 }
136
137 /**
138 * Get the median of a given key.
139 *
140 * @param string|array|null $key
141 * @return mixed
142 */
143 public function median($key = null)
144 {
145 return $this->collect()->median($key);
146 }
147
148 /**
149 * Get the mode of a given key.
150 *
151 * @param string|array|null $key
152 * @return array|null
153 */
154 public function mode($key = null)
155 {
156 return $this->collect()->mode($key);
157 }
158
159 /**
160 * Collapse the collection of items into a single array.
161 *
162 * @return static
163 */
164 public function collapse()
165 {
166 return new static(function () {
167 foreach ($this as $values) {
168 if (is_array($values) || $values instanceof Enumerable) {
169 foreach ($values as $value) {
170 yield $value;
171 }
172 }
173 }
174 });
175 }
176
177 /**
178 * Determine if an item exists in the enumerable.
179 *
180 * @param mixed $key
181 * @param mixed $operator
182 * @param mixed $value
183 * @return bool
184 */
185 public function contains($key, $operator = null, $value = null)
186 {
187 if (func_num_args() === 1 && $this->useAsCallable($key)) {
188 $placeholder = new stdClass;
189
190 return $this->first($key, $placeholder) !== $placeholder;
191 }
192
193 if (func_num_args() === 1) {
194 $needle = $key;
195
196 foreach ($this as $value) {
197 if ($value == $needle) {
198 return true;
199 }
200 }
201
202 return false;
203 }
204
205 return $this->contains($this->operatorForWhere(...func_get_args()));
206 }
207
208 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100209 * Determine if an item is not contained in the enumerable.
210 *
211 * @param mixed $key
212 * @param mixed $operator
213 * @param mixed $value
214 * @return bool
215 */
216 public function doesntContain($key, $operator = null, $value = null)
217 {
218 return ! $this->contains(...func_get_args());
219 }
220
221 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200222 * Cross join the given iterables, returning all possible permutations.
223 *
224 * @param array ...$arrays
225 * @return static
226 */
227 public function crossJoin(...$arrays)
228 {
229 return $this->passthru('crossJoin', func_get_args());
230 }
231
232 /**
233 * Count the number of items in the collection by a field or using a callback.
234 *
235 * @param callable|string $countBy
236 * @return static
237 */
238 public function countBy($countBy = null)
239 {
240 $countBy = is_null($countBy)
241 ? $this->identity()
242 : $this->valueRetriever($countBy);
243
244 return new static(function () use ($countBy) {
245 $counts = [];
246
247 foreach ($this as $key => $value) {
248 $group = $countBy($value, $key);
249
250 if (empty($counts[$group])) {
251 $counts[$group] = 0;
252 }
253
254 $counts[$group]++;
255 }
256
257 yield from $counts;
258 });
259 }
260
261 /**
262 * Get the items that are not present in the given items.
263 *
264 * @param mixed $items
265 * @return static
266 */
267 public function diff($items)
268 {
269 return $this->passthru('diff', func_get_args());
270 }
271
272 /**
273 * Get the items that are not present in the given items, using the callback.
274 *
275 * @param mixed $items
276 * @param callable $callback
277 * @return static
278 */
279 public function diffUsing($items, callable $callback)
280 {
281 return $this->passthru('diffUsing', func_get_args());
282 }
283
284 /**
285 * Get the items whose keys and values are not present in the given items.
286 *
287 * @param mixed $items
288 * @return static
289 */
290 public function diffAssoc($items)
291 {
292 return $this->passthru('diffAssoc', func_get_args());
293 }
294
295 /**
296 * Get the items whose keys and values are not present in the given items, using the callback.
297 *
298 * @param mixed $items
299 * @param callable $callback
300 * @return static
301 */
302 public function diffAssocUsing($items, callable $callback)
303 {
304 return $this->passthru('diffAssocUsing', func_get_args());
305 }
306
307 /**
308 * Get the items whose keys are not present in the given items.
309 *
310 * @param mixed $items
311 * @return static
312 */
313 public function diffKeys($items)
314 {
315 return $this->passthru('diffKeys', func_get_args());
316 }
317
318 /**
319 * Get the items whose keys are not present in the given items, using the callback.
320 *
321 * @param mixed $items
322 * @param callable $callback
323 * @return static
324 */
325 public function diffKeysUsing($items, callable $callback)
326 {
327 return $this->passthru('diffKeysUsing', func_get_args());
328 }
329
330 /**
331 * Retrieve duplicate items.
332 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100333 * @param callable|string|null $callback
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200334 * @param bool $strict
335 * @return static
336 */
337 public function duplicates($callback = null, $strict = false)
338 {
339 return $this->passthru('duplicates', func_get_args());
340 }
341
342 /**
343 * Retrieve duplicate items using strict comparison.
344 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100345 * @param callable|string|null $callback
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200346 * @return static
347 */
348 public function duplicatesStrict($callback = null)
349 {
350 return $this->passthru('duplicatesStrict', func_get_args());
351 }
352
353 /**
354 * Get all items except for those with the specified keys.
355 *
356 * @param mixed $keys
357 * @return static
358 */
359 public function except($keys)
360 {
361 return $this->passthru('except', func_get_args());
362 }
363
364 /**
365 * Run a filter over each of the items.
366 *
367 * @param callable|null $callback
368 * @return static
369 */
370 public function filter(callable $callback = null)
371 {
372 if (is_null($callback)) {
373 $callback = function ($value) {
374 return (bool) $value;
375 };
376 }
377
378 return new static(function () use ($callback) {
379 foreach ($this as $key => $value) {
380 if ($callback($value, $key)) {
381 yield $key => $value;
382 }
383 }
384 });
385 }
386
387 /**
388 * Get the first item from the enumerable passing the given truth test.
389 *
390 * @param callable|null $callback
391 * @param mixed $default
392 * @return mixed
393 */
394 public function first(callable $callback = null, $default = null)
395 {
396 $iterator = $this->getIterator();
397
398 if (is_null($callback)) {
399 if (! $iterator->valid()) {
400 return value($default);
401 }
402
403 return $iterator->current();
404 }
405
406 foreach ($iterator as $key => $value) {
407 if ($callback($value, $key)) {
408 return $value;
409 }
410 }
411
412 return value($default);
413 }
414
415 /**
416 * Get a flattened list of the items in the collection.
417 *
418 * @param int $depth
419 * @return static
420 */
421 public function flatten($depth = INF)
422 {
423 $instance = new static(function () use ($depth) {
424 foreach ($this as $item) {
425 if (! is_array($item) && ! $item instanceof Enumerable) {
426 yield $item;
427 } elseif ($depth === 1) {
428 yield from $item;
429 } else {
430 yield from (new static($item))->flatten($depth - 1);
431 }
432 }
433 });
434
435 return $instance->values();
436 }
437
438 /**
439 * Flip the items in the collection.
440 *
441 * @return static
442 */
443 public function flip()
444 {
445 return new static(function () {
446 foreach ($this as $key => $value) {
447 yield $value => $key;
448 }
449 });
450 }
451
452 /**
453 * Get an item by key.
454 *
455 * @param mixed $key
456 * @param mixed $default
457 * @return mixed
458 */
459 public function get($key, $default = null)
460 {
461 if (is_null($key)) {
462 return;
463 }
464
465 foreach ($this as $outerKey => $outerValue) {
466 if ($outerKey == $key) {
467 return $outerValue;
468 }
469 }
470
471 return value($default);
472 }
473
474 /**
475 * Group an associative array by a field or using a callback.
476 *
477 * @param array|callable|string $groupBy
478 * @param bool $preserveKeys
479 * @return static
480 */
481 public function groupBy($groupBy, $preserveKeys = false)
482 {
483 return $this->passthru('groupBy', func_get_args());
484 }
485
486 /**
487 * Key an associative array by a field or using a callback.
488 *
489 * @param callable|string $keyBy
490 * @return static
491 */
492 public function keyBy($keyBy)
493 {
494 return new static(function () use ($keyBy) {
495 $keyBy = $this->valueRetriever($keyBy);
496
497 foreach ($this as $key => $item) {
498 $resolvedKey = $keyBy($item, $key);
499
500 if (is_object($resolvedKey)) {
501 $resolvedKey = (string) $resolvedKey;
502 }
503
504 yield $resolvedKey => $item;
505 }
506 });
507 }
508
509 /**
510 * Determine if an item exists in the collection by key.
511 *
512 * @param mixed $key
513 * @return bool
514 */
515 public function has($key)
516 {
517 $keys = array_flip(is_array($key) ? $key : func_get_args());
518 $count = count($keys);
519
520 foreach ($this as $key => $value) {
521 if (array_key_exists($key, $keys) && --$count == 0) {
522 return true;
523 }
524 }
525
526 return false;
527 }
528
529 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100530 * Determine if any of the keys exist in the collection.
531 *
532 * @param mixed $key
533 * @return bool
534 */
535 public function hasAny($key)
536 {
537 $keys = array_flip(is_array($key) ? $key : func_get_args());
538
539 foreach ($this as $key => $value) {
540 if (array_key_exists($key, $keys)) {
541 return true;
542 }
543 }
544
545 return false;
546 }
547
548 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200549 * Concatenate values of a given key as a string.
550 *
551 * @param string $value
552 * @param string|null $glue
553 * @return string
554 */
555 public function implode($value, $glue = null)
556 {
557 return $this->collect()->implode(...func_get_args());
558 }
559
560 /**
561 * Intersect the collection with the given items.
562 *
563 * @param mixed $items
564 * @return static
565 */
566 public function intersect($items)
567 {
568 return $this->passthru('intersect', func_get_args());
569 }
570
571 /**
572 * Intersect the collection with the given items by key.
573 *
574 * @param mixed $items
575 * @return static
576 */
577 public function intersectByKeys($items)
578 {
579 return $this->passthru('intersectByKeys', func_get_args());
580 }
581
582 /**
583 * Determine if the items are empty or not.
584 *
585 * @return bool
586 */
587 public function isEmpty()
588 {
589 return ! $this->getIterator()->valid();
590 }
591
592 /**
593 * Determine if the collection contains a single item.
594 *
595 * @return bool
596 */
597 public function containsOneItem()
598 {
599 return $this->take(2)->count() === 1;
600 }
601
602 /**
603 * Join all items from the collection using a string. The final items can use a separate glue string.
604 *
605 * @param string $glue
606 * @param string $finalGlue
607 * @return string
608 */
609 public function join($glue, $finalGlue = '')
610 {
611 return $this->collect()->join(...func_get_args());
612 }
613
614 /**
615 * Get the keys of the collection items.
616 *
617 * @return static
618 */
619 public function keys()
620 {
621 return new static(function () {
622 foreach ($this as $key => $value) {
623 yield $key;
624 }
625 });
626 }
627
628 /**
629 * Get the last item from the collection.
630 *
631 * @param callable|null $callback
632 * @param mixed $default
633 * @return mixed
634 */
635 public function last(callable $callback = null, $default = null)
636 {
637 $needle = $placeholder = new stdClass;
638
639 foreach ($this as $key => $value) {
640 if (is_null($callback) || $callback($value, $key)) {
641 $needle = $value;
642 }
643 }
644
645 return $needle === $placeholder ? value($default) : $needle;
646 }
647
648 /**
649 * Get the values of a given key.
650 *
651 * @param string|array $value
652 * @param string|null $key
653 * @return static
654 */
655 public function pluck($value, $key = null)
656 {
657 return new static(function () use ($value, $key) {
658 [$value, $key] = $this->explodePluckParameters($value, $key);
659
660 foreach ($this as $item) {
661 $itemValue = data_get($item, $value);
662
663 if (is_null($key)) {
664 yield $itemValue;
665 } else {
666 $itemKey = data_get($item, $key);
667
668 if (is_object($itemKey) && method_exists($itemKey, '__toString')) {
669 $itemKey = (string) $itemKey;
670 }
671
672 yield $itemKey => $itemValue;
673 }
674 }
675 });
676 }
677
678 /**
679 * Run a map over each of the items.
680 *
681 * @param callable $callback
682 * @return static
683 */
684 public function map(callable $callback)
685 {
686 return new static(function () use ($callback) {
687 foreach ($this as $key => $value) {
688 yield $key => $callback($value, $key);
689 }
690 });
691 }
692
693 /**
694 * Run a dictionary map over the items.
695 *
696 * The callback should return an associative array with a single key/value pair.
697 *
698 * @param callable $callback
699 * @return static
700 */
701 public function mapToDictionary(callable $callback)
702 {
703 return $this->passthru('mapToDictionary', func_get_args());
704 }
705
706 /**
707 * Run an associative map over each of the items.
708 *
709 * The callback should return an associative array with a single key/value pair.
710 *
711 * @param callable $callback
712 * @return static
713 */
714 public function mapWithKeys(callable $callback)
715 {
716 return new static(function () use ($callback) {
717 foreach ($this as $key => $value) {
718 yield from $callback($value, $key);
719 }
720 });
721 }
722
723 /**
724 * Merge the collection with the given items.
725 *
726 * @param mixed $items
727 * @return static
728 */
729 public function merge($items)
730 {
731 return $this->passthru('merge', func_get_args());
732 }
733
734 /**
735 * Recursively merge the collection with the given items.
736 *
737 * @param mixed $items
738 * @return static
739 */
740 public function mergeRecursive($items)
741 {
742 return $this->passthru('mergeRecursive', func_get_args());
743 }
744
745 /**
746 * Create a collection by using this collection for keys and another for its values.
747 *
748 * @param mixed $values
749 * @return static
750 */
751 public function combine($values)
752 {
753 return new static(function () use ($values) {
754 $values = $this->makeIterator($values);
755
756 $errorMessage = 'Both parameters should have an equal number of elements';
757
758 foreach ($this as $key) {
759 if (! $values->valid()) {
760 trigger_error($errorMessage, E_USER_WARNING);
761
762 break;
763 }
764
765 yield $key => $values->current();
766
767 $values->next();
768 }
769
770 if ($values->valid()) {
771 trigger_error($errorMessage, E_USER_WARNING);
772 }
773 });
774 }
775
776 /**
777 * Union the collection with the given items.
778 *
779 * @param mixed $items
780 * @return static
781 */
782 public function union($items)
783 {
784 return $this->passthru('union', func_get_args());
785 }
786
787 /**
788 * Create a new collection consisting of every n-th element.
789 *
790 * @param int $step
791 * @param int $offset
792 * @return static
793 */
794 public function nth($step, $offset = 0)
795 {
796 return new static(function () use ($step, $offset) {
797 $position = 0;
798
799 foreach ($this as $item) {
800 if ($position % $step === $offset) {
801 yield $item;
802 }
803
804 $position++;
805 }
806 });
807 }
808
809 /**
810 * Get the items with the specified keys.
811 *
812 * @param mixed $keys
813 * @return static
814 */
815 public function only($keys)
816 {
817 if ($keys instanceof Enumerable) {
818 $keys = $keys->all();
819 } elseif (! is_null($keys)) {
820 $keys = is_array($keys) ? $keys : func_get_args();
821 }
822
823 return new static(function () use ($keys) {
824 if (is_null($keys)) {
825 yield from $this;
826 } else {
827 $keys = array_flip($keys);
828
829 foreach ($this as $key => $value) {
830 if (array_key_exists($key, $keys)) {
831 yield $key => $value;
832
833 unset($keys[$key]);
834
835 if (empty($keys)) {
836 break;
837 }
838 }
839 }
840 }
841 });
842 }
843
844 /**
845 * Push all of the given items onto the collection.
846 *
847 * @param iterable $source
848 * @return static
849 */
850 public function concat($source)
851 {
852 return (new static(function () use ($source) {
853 yield from $this;
854 yield from $source;
855 }))->values();
856 }
857
858 /**
859 * Get one or a specified number of items randomly from the collection.
860 *
861 * @param int|null $number
862 * @return static|mixed
863 *
864 * @throws \InvalidArgumentException
865 */
866 public function random($number = null)
867 {
868 $result = $this->collect()->random(...func_get_args());
869
870 return is_null($number) ? $result : new static($result);
871 }
872
873 /**
874 * Replace the collection items with the given items.
875 *
876 * @param mixed $items
877 * @return static
878 */
879 public function replace($items)
880 {
881 return new static(function () use ($items) {
882 $items = $this->getArrayableItems($items);
883
884 foreach ($this as $key => $value) {
885 if (array_key_exists($key, $items)) {
886 yield $key => $items[$key];
887
888 unset($items[$key]);
889 } else {
890 yield $key => $value;
891 }
892 }
893
894 foreach ($items as $key => $value) {
895 yield $key => $value;
896 }
897 });
898 }
899
900 /**
901 * Recursively replace the collection items with the given items.
902 *
903 * @param mixed $items
904 * @return static
905 */
906 public function replaceRecursive($items)
907 {
908 return $this->passthru('replaceRecursive', func_get_args());
909 }
910
911 /**
912 * Reverse items order.
913 *
914 * @return static
915 */
916 public function reverse()
917 {
918 return $this->passthru('reverse', func_get_args());
919 }
920
921 /**
922 * Search the collection for a given value and return the corresponding key if successful.
923 *
924 * @param mixed $value
925 * @param bool $strict
926 * @return mixed
927 */
928 public function search($value, $strict = false)
929 {
930 $predicate = $this->useAsCallable($value)
931 ? $value
932 : function ($item) use ($value, $strict) {
933 return $strict ? $item === $value : $item == $value;
934 };
935
936 foreach ($this as $key => $item) {
937 if ($predicate($item, $key)) {
938 return $key;
939 }
940 }
941
942 return false;
943 }
944
945 /**
946 * Shuffle the items in the collection.
947 *
948 * @param int|null $seed
949 * @return static
950 */
951 public function shuffle($seed = null)
952 {
953 return $this->passthru('shuffle', func_get_args());
954 }
955
956 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100957 * Create chunks representing a "sliding window" view of the items in the collection.
958 *
959 * @param int $size
960 * @param int $step
961 * @return static
962 */
963 public function sliding($size = 2, $step = 1)
964 {
965 return new static(function () use ($size, $step) {
966 $iterator = $this->getIterator();
967
968 $chunk = [];
969
970 while ($iterator->valid()) {
971 $chunk[$iterator->key()] = $iterator->current();
972
973 if (count($chunk) == $size) {
974 yield tap(new static($chunk), function () use (&$chunk, $step) {
975 $chunk = array_slice($chunk, $step, null, true);
976 });
977
978 // If the $step between chunks is bigger than each chunk's $size
979 // we will skip the extra items (which should never be in any
980 // chunk) before we continue to the next chunk in the loop.
981 if ($step > $size) {
982 $skip = $step - $size;
983
984 for ($i = 0; $i < $skip && $iterator->valid(); $i++) {
985 $iterator->next();
986 }
987 }
988 }
989
990 $iterator->next();
991 }
992 });
993 }
994
995 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200996 * Skip the first {$count} items.
997 *
998 * @param int $count
999 * @return static
1000 */
1001 public function skip($count)
1002 {
1003 return new static(function () use ($count) {
1004 $iterator = $this->getIterator();
1005
1006 while ($iterator->valid() && $count--) {
1007 $iterator->next();
1008 }
1009
1010 while ($iterator->valid()) {
1011 yield $iterator->key() => $iterator->current();
1012
1013 $iterator->next();
1014 }
1015 });
1016 }
1017
1018 /**
1019 * Skip items in the collection until the given condition is met.
1020 *
1021 * @param mixed $value
1022 * @return static
1023 */
1024 public function skipUntil($value)
1025 {
1026 $callback = $this->useAsCallable($value) ? $value : $this->equality($value);
1027
1028 return $this->skipWhile($this->negate($callback));
1029 }
1030
1031 /**
1032 * Skip items in the collection while the given condition is met.
1033 *
1034 * @param mixed $value
1035 * @return static
1036 */
1037 public function skipWhile($value)
1038 {
1039 $callback = $this->useAsCallable($value) ? $value : $this->equality($value);
1040
1041 return new static(function () use ($callback) {
1042 $iterator = $this->getIterator();
1043
1044 while ($iterator->valid() && $callback($iterator->current(), $iterator->key())) {
1045 $iterator->next();
1046 }
1047
1048 while ($iterator->valid()) {
1049 yield $iterator->key() => $iterator->current();
1050
1051 $iterator->next();
1052 }
1053 });
1054 }
1055
1056 /**
1057 * Get a slice of items from the enumerable.
1058 *
1059 * @param int $offset
1060 * @param int|null $length
1061 * @return static
1062 */
1063 public function slice($offset, $length = null)
1064 {
1065 if ($offset < 0 || $length < 0) {
1066 return $this->passthru('slice', func_get_args());
1067 }
1068
1069 $instance = $this->skip($offset);
1070
1071 return is_null($length) ? $instance : $instance->take($length);
1072 }
1073
1074 /**
1075 * Split a collection into a certain number of groups.
1076 *
1077 * @param int $numberOfGroups
1078 * @return static
1079 */
1080 public function split($numberOfGroups)
1081 {
1082 return $this->passthru('split', func_get_args());
1083 }
1084
1085 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001086 * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
1087 *
1088 * @param mixed $key
1089 * @param mixed $operator
1090 * @param mixed $value
1091 * @return mixed
1092 *
1093 * @throws \Tightenco\Collect\Support\ItemNotFoundException
1094 * @throws \Tightenco\Collect\Support\MultipleItemsFoundException
1095 */
1096 public function sole($key = null, $operator = null, $value = null)
1097 {
1098 $filter = func_num_args() > 1
1099 ? $this->operatorForWhere(...func_get_args())
1100 : $key;
1101
1102 return $this
1103 ->when($filter)
1104 ->filter($filter)
1105 ->take(2)
1106 ->collect()
1107 ->sole();
1108 }
1109
1110 /**
1111 * Get the first item in the collection but throw an exception if no matching items exist.
1112 *
1113 * @param mixed $key
1114 * @param mixed $operator
1115 * @param mixed $value
1116 * @return mixed
1117 *
1118 * @throws \Tightenco\Collect\Support\ItemNotFoundException
1119 */
1120 public function firstOrFail($key = null, $operator = null, $value = null)
1121 {
1122 $filter = func_num_args() > 1
1123 ? $this->operatorForWhere(...func_get_args())
1124 : $key;
1125
1126 return $this
1127 ->when($filter)
1128 ->filter($filter)
1129 ->take(1)
1130 ->collect()
1131 ->firstOrFail();
1132 }
1133
1134 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001135 * Chunk the collection into chunks of the given size.
1136 *
1137 * @param int $size
1138 * @return static
1139 */
1140 public function chunk($size)
1141 {
1142 if ($size <= 0) {
1143 return static::empty();
1144 }
1145
1146 return new static(function () use ($size) {
1147 $iterator = $this->getIterator();
1148
1149 while ($iterator->valid()) {
1150 $chunk = [];
1151
1152 while (true) {
1153 $chunk[$iterator->key()] = $iterator->current();
1154
1155 if (count($chunk) < $size) {
1156 $iterator->next();
1157
1158 if (! $iterator->valid()) {
1159 break;
1160 }
1161 } else {
1162 break;
1163 }
1164 }
1165
1166 yield new static($chunk);
1167
1168 $iterator->next();
1169 }
1170 });
1171 }
1172
1173 /**
1174 * Split a collection into a certain number of groups, and fill the first groups completely.
1175 *
1176 * @param int $numberOfGroups
1177 * @return static
1178 */
1179 public function splitIn($numberOfGroups)
1180 {
1181 return $this->chunk(ceil($this->count() / $numberOfGroups));
1182 }
1183
1184 /**
1185 * Chunk the collection into chunks with a callback.
1186 *
1187 * @param callable $callback
1188 * @return static
1189 */
1190 public function chunkWhile(callable $callback)
1191 {
1192 return new static(function () use ($callback) {
1193 $iterator = $this->getIterator();
1194
1195 $chunk = new Collection;
1196
1197 if ($iterator->valid()) {
1198 $chunk[$iterator->key()] = $iterator->current();
1199
1200 $iterator->next();
1201 }
1202
1203 while ($iterator->valid()) {
1204 if (! $callback($iterator->current(), $iterator->key(), $chunk)) {
1205 yield new static($chunk);
1206
1207 $chunk = new Collection;
1208 }
1209
1210 $chunk[$iterator->key()] = $iterator->current();
1211
1212 $iterator->next();
1213 }
1214
1215 if ($chunk->isNotEmpty()) {
1216 yield new static($chunk);
1217 }
1218 });
1219 }
1220
1221 /**
1222 * Sort through each item with a callback.
1223 *
1224 * @param callable|null|int $callback
1225 * @return static
1226 */
1227 public function sort($callback = null)
1228 {
1229 return $this->passthru('sort', func_get_args());
1230 }
1231
1232 /**
1233 * Sort items in descending order.
1234 *
1235 * @param int $options
1236 * @return static
1237 */
1238 public function sortDesc($options = SORT_REGULAR)
1239 {
1240 return $this->passthru('sortDesc', func_get_args());
1241 }
1242
1243 /**
1244 * Sort the collection using the given callback.
1245 *
1246 * @param callable|string $callback
1247 * @param int $options
1248 * @param bool $descending
1249 * @return static
1250 */
1251 public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
1252 {
1253 return $this->passthru('sortBy', func_get_args());
1254 }
1255
1256 /**
1257 * Sort the collection in descending order using the given callback.
1258 *
1259 * @param callable|string $callback
1260 * @param int $options
1261 * @return static
1262 */
1263 public function sortByDesc($callback, $options = SORT_REGULAR)
1264 {
1265 return $this->passthru('sortByDesc', func_get_args());
1266 }
1267
1268 /**
1269 * Sort the collection keys.
1270 *
1271 * @param int $options
1272 * @param bool $descending
1273 * @return static
1274 */
1275 public function sortKeys($options = SORT_REGULAR, $descending = false)
1276 {
1277 return $this->passthru('sortKeys', func_get_args());
1278 }
1279
1280 /**
1281 * Sort the collection keys in descending order.
1282 *
1283 * @param int $options
1284 * @return static
1285 */
1286 public function sortKeysDesc($options = SORT_REGULAR)
1287 {
1288 return $this->passthru('sortKeysDesc', func_get_args());
1289 }
1290
1291 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001292 * Sort the collection keys using a callback.
1293 *
1294 * @param callable $callback
1295 * @return static
1296 */
1297 public function sortKeysUsing(callable $callback)
1298 {
1299 return $this->passthru('sortKeysUsing', func_get_args());
1300 }
1301
1302 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001303 * Take the first or last {$limit} items.
1304 *
1305 * @param int $limit
1306 * @return static
1307 */
1308 public function take($limit)
1309 {
1310 if ($limit < 0) {
1311 return $this->passthru('take', func_get_args());
1312 }
1313
1314 return new static(function () use ($limit) {
1315 $iterator = $this->getIterator();
1316
1317 while ($limit--) {
1318 if (! $iterator->valid()) {
1319 break;
1320 }
1321
1322 yield $iterator->key() => $iterator->current();
1323
1324 if ($limit) {
1325 $iterator->next();
1326 }
1327 }
1328 });
1329 }
1330
1331 /**
1332 * Take items in the collection until the given condition is met.
1333 *
1334 * @param mixed $value
1335 * @return static
1336 */
1337 public function takeUntil($value)
1338 {
1339 $callback = $this->useAsCallable($value) ? $value : $this->equality($value);
1340
1341 return new static(function () use ($callback) {
1342 foreach ($this as $key => $item) {
1343 if ($callback($item, $key)) {
1344 break;
1345 }
1346
1347 yield $key => $item;
1348 }
1349 });
1350 }
1351
1352 /**
1353 * Take items in the collection until a given point in time.
1354 *
1355 * @param \DateTimeInterface $timeout
1356 * @return static
1357 */
1358 public function takeUntilTimeout(DateTimeInterface $timeout)
1359 {
1360 $timeout = $timeout->getTimestamp();
1361
1362 return $this->takeWhile(function () use ($timeout) {
1363 return $this->now() < $timeout;
1364 });
1365 }
1366
1367 /**
1368 * Take items in the collection while the given condition is met.
1369 *
1370 * @param mixed $value
1371 * @return static
1372 */
1373 public function takeWhile($value)
1374 {
1375 $callback = $this->useAsCallable($value) ? $value : $this->equality($value);
1376
1377 return $this->takeUntil(function ($item, $key) use ($callback) {
1378 return ! $callback($item, $key);
1379 });
1380 }
1381
1382 /**
1383 * Pass each item in the collection to the given callback, lazily.
1384 *
1385 * @param callable $callback
1386 * @return static
1387 */
1388 public function tapEach(callable $callback)
1389 {
1390 return new static(function () use ($callback) {
1391 foreach ($this as $key => $value) {
1392 $callback($value, $key);
1393
1394 yield $key => $value;
1395 }
1396 });
1397 }
1398
1399 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001400 * Convert a flatten "dot" notation array into an expanded array.
1401 *
1402 * @return static
1403 */
1404 public function undot()
1405 {
1406 return $this->passthru('undot', []);
1407 }
1408
1409 /**
1410 * Return only unique items from the collection array.
1411 *
1412 * @param string|callable|null $key
1413 * @param bool $strict
1414 * @return static
1415 */
1416 public function unique($key = null, $strict = false)
1417 {
1418 $callback = $this->valueRetriever($key);
1419
1420 return new static(function () use ($callback, $strict) {
1421 $exists = [];
1422
1423 foreach ($this as $key => $item) {
1424 if (! in_array($id = $callback($item, $key), $exists, $strict)) {
1425 yield $key => $item;
1426
1427 $exists[] = $id;
1428 }
1429 }
1430 });
1431 }
1432
1433 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001434 * Reset the keys on the underlying array.
1435 *
1436 * @return static
1437 */
1438 public function values()
1439 {
1440 return new static(function () {
1441 foreach ($this as $item) {
1442 yield $item;
1443 }
1444 });
1445 }
1446
1447 /**
1448 * Zip the collection together with one or more arrays.
1449 *
1450 * e.g. new LazyCollection([1, 2, 3])->zip([4, 5, 6]);
1451 * => [[1, 4], [2, 5], [3, 6]]
1452 *
1453 * @param mixed ...$items
1454 * @return static
1455 */
1456 public function zip($items)
1457 {
1458 $iterables = func_get_args();
1459
1460 return new static(function () use ($iterables) {
1461 $iterators = Collection::make($iterables)->map(function ($iterable) {
1462 return $this->makeIterator($iterable);
1463 })->prepend($this->getIterator());
1464
1465 while ($iterators->contains->valid()) {
1466 yield new static($iterators->map->current());
1467
1468 $iterators->each->next();
1469 }
1470 });
1471 }
1472
1473 /**
1474 * Pad collection to the specified length with a value.
1475 *
1476 * @param int $size
1477 * @param mixed $value
1478 * @return static
1479 */
1480 public function pad($size, $value)
1481 {
1482 if ($size < 0) {
1483 return $this->passthru('pad', func_get_args());
1484 }
1485
1486 return new static(function () use ($size, $value) {
1487 $yielded = 0;
1488
1489 foreach ($this as $index => $item) {
1490 yield $index => $item;
1491
1492 $yielded++;
1493 }
1494
1495 while ($yielded++ < $size) {
1496 yield $value;
1497 }
1498 });
1499 }
1500
1501 /**
1502 * Get the values iterator.
1503 *
1504 * @return \Traversable
1505 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001506 #[\ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001507 public function getIterator()
1508 {
1509 return $this->makeIterator($this->source);
1510 }
1511
1512 /**
1513 * Count the number of items in the collection.
1514 *
1515 * @return int
1516 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001517 #[\ReturnTypeWillChange]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001518 public function count()
1519 {
1520 if (is_array($this->source)) {
1521 return count($this->source);
1522 }
1523
1524 return iterator_count($this->getIterator());
1525 }
1526
1527 /**
1528 * Make an iterator from the given source.
1529 *
1530 * @param mixed $source
1531 * @return \Traversable
1532 */
1533 protected function makeIterator($source)
1534 {
1535 if ($source instanceof IteratorAggregate) {
1536 return $source->getIterator();
1537 }
1538
1539 if (is_array($source)) {
1540 return new ArrayIterator($source);
1541 }
1542
1543 return $source();
1544 }
1545
1546 /**
1547 * Explode the "value" and "key" arguments passed to "pluck".
1548 *
1549 * @param string|array $value
1550 * @param string|array|null $key
1551 * @return array
1552 */
1553 protected function explodePluckParameters($value, $key)
1554 {
1555 $value = is_string($value) ? explode('.', $value) : $value;
1556
1557 $key = is_null($key) || is_array($key) ? $key : explode('.', $key);
1558
1559 return [$value, $key];
1560 }
1561
1562 /**
1563 * Pass this lazy collection through a method on the collection class.
1564 *
1565 * @param string $method
1566 * @param array $params
1567 * @return static
1568 */
1569 protected function passthru($method, array $params)
1570 {
1571 return new static(function () use ($method, $params) {
1572 yield from $this->collect()->$method(...$params);
1573 });
1574 }
1575
1576 /**
1577 * Get the current time.
1578 *
1579 * @return int
1580 */
1581 protected function now()
1582 {
1583 return time();
1584 }
1585}