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