Completed
Push — master ( da481c...f190df )
by Luke
19:50 queued 09:51
created

Collection::product()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 0
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 3
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * nozavroni/collect
4
 *
5
 * This is a basic utility library for PHP5.6+ with special emphesis on Collections.
6
 *
7
 * @author Luke Visinoni <[email protected]>
8
 * @copyright (c) 2018 Luke Visinoni <[email protected]>
9
 * @license MIT (see LICENSE file)
10
 */
11
namespace Noz\Collection;
12
13
use Countable;
14
use JsonSerializable;
15
use Iterator;
16
use ArrayAccess;
17
use RuntimeException;
18
use Traversable;
19
20
use function Noz\is_traversable,
21
             Noz\to_array,
22
             Noz\to_numeric;
23
24
/**
25
 * Nozavroni Collection
26
 *
27
 * Basically an array wrapper with a bunch of super useful methods for working with its items and/or create new collections from its items.
28
 *
29
 * @note None of the methods in this class have a $preserveKeys param. That is by design. I don't think it's necessary.
30
 *       Instead, keys are ALWAYS preserved and if you want to NOT preserve keys, simply call Collection::values().
31
 *
32
 * @note The signature for callbacks throughout this class, unless otherwise stated, will be:
33
 *       (mixed $value, mixed $key, int $index), where $index will be simply a numeric value starting at zero, that is
34
 *       incremented by one for each successive call to the callback. The other two arguments should be obvious. The
35
 *       expected return value will depend on the method for which it is being used.
36
 */
37
class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
38
{
39
    /** @var array The items for this collection */
40
    protected $items;
41
42
    /**
43
     * Collection constructor.
44
     * 
45
     * Although most methods in this class are more forgiving and accept anything that is traversable rather than
46
     * strictly an array, the constructor is an exception. It expects an array. If you have an Array-ish object and it 
47
     * is traversable, you may use the factory method instead to generate a collection from it.
48
     *
49
     * @param array $items The items to include in the collection
50
     */
51 122
    public function __construct(array $items = [])
52
    {
53 122
        $this->items = $items;
54 122
        $this->rewind();
55 122
    }
56
57
    /**
58
     * Generate a collection from any iterable
59
     *
60
     * This is the method used internally to generate new collections. This allows for this class to be extended if 
61
     * necessary. This way, the child class will use its own factory method to generate new collections (or otherwise 
62
     * use this one).
63
     *
64
     * @param array|Traversable $items The items to include in the collection
65
     *
66
     * @return Collection
67
     */
68 49
    public static function factory($items = null)
69
    {
70 49
        return new Collection(to_array($items, true));
71
    }
72
73
    /**
74
     * Get collection as an array
75
     *
76
     * @return array
77
     */
78 55
    public function toArray()
79
    {
80 55
        return $this->items;
81
    }
82
83
    /**
84
     * Determine if collection has a given key
85
     *
86
     * @param mixed $key The key to check for
87
     *
88
     * @return bool
89
     */
90 68
    public function has($key)
91
    {
92 68
        return isset($this->items[$key]) || array_key_exists($key, $this->items);
93
    }
94
95
    /**
96
     * Determine if collection has a value at given position
97
     *
98
     * If the $position argument is positive, counting will start at the beginning and start from one (rather than zero).
99
     * If $position is negative, counting will start at the end and work backwards. This is not the same as array
100
     * indexing, as that begins from zero.
101
     *
102
     * @param int $position The numeric position to check for a value at
103
     *
104
     * @return bool
105
     */
106 2
    public function hasValueAt($position)
107
    {
108
        try {
109 2
            $this->getKeyAt($position);
110 2
            return true;
111 2
        } catch (RuntimeException $e) {
112 2
            return false;
113
        }
114
    }
115
116
    /**
117
     * Get key at given position
118
     *
119
     * If the $position argument is positive, counting will start at the beginning and start from one (rather than zero).
120
     * If $position is negative, counting will start at the end and work backwards. If an item exists at the specified
121
     * position, its key will be returned. Otherwise a RuntimeException will be thrown.
122
     *
123
     * @param int $position The numeric position to get a key at
124
     *
125
     * @return mixed
126
     *
127
     * @throws RuntimeException
128
     */
129 8
    public function getKeyAt($position)
130
    {
131 8
        $collection = $this;
132 8
        if ($position < 0) {
133 3
            $collection = $this->reverse();
134 3
        }
135 8
        $i = 1;
136 8
        foreach ($collection as $key => $val) {
137 8
            if (abs($position) == $i++) {
138 7
                return $key;
139
            }
140 8
        }
141 3
        throw new RuntimeException("No key at position {$position}");
142
    }
143
144
    /**
145
     * Get value at given position
146
     *
147
     * If the $position argument is positive, counting will start at the beginning and start from one (rather than zero).
148
     * If $position is negative, counting will start at the end and work backwards. If an item exists at the specified
149
     * position, its value will be returned. Otherwise a RuntimeException will be thrown.
150
     *
151
     * @param int $position The numeric position to get a value at
152
     *
153
     * @return mixed
154
     *
155
     * @throws RuntimeException
156
     */
157 4
    public function getValueAt($position)
158
    {
159 4
        return $this->get($this->getKeyAt($position));
160
    }
161
162
    /**
163
     * Get the key of first item exactly equal to $item
164
     *
165
     * Searches the collection for an item exactly equal to $item, returning its key if found. If a callback is provided
166
     * rather than a value, it will be passed the conventional three arguments ($value, $key, $index) and returning true
167
     * from this callback would be considered a "match". If no match is found, a RuntimeException will be thrown.
168
     *
169
     * @param mixed|callable $item The value to look for or a callback
170
     *
171
     * @throws RuntimeException
172
     *
173
     * @return mixed
174
     */
175 3
    public function keyOf($item)
176
    {
177 3
        $index = 0;
178 3
        foreach ($this as $key => $val) {
179 3
            if (is_callable($item)) {
180 1
                if ($item($val, $key, $index++)) {
181 1
                    return $key;
182
                }
183 3
            } elseif ($item === $val) {
184 1
                return $key;
185
            }
186 3
        }
187
188 1
        throw new RuntimeException("Could not find item equal to '{$item}'");
189
    }
190
191
    /**
192
     * Get the numeric index of first item exactly equal to $item
193
     *
194
     * Searches the collection for an item exactly equal to $item, returning its numeric index if found. If a callback
195
     * is provided rather than a value, it will be passed the conventional three arguments ($value, $key, $index) and
196
     * returning true from this callback would be considered a "match". If no match is found, a RuntimeException will be
197
     * thrown.
198
     *
199
     * @param mixed|callable $item The value to look for or a callback
200
     *
201
     * @throws RuntimeException
202
     *
203
     * @return int
204
     */
205 3
    public function indexOf($item)
206
    {
207 3
        $index = 0;
208 3
        foreach ($this as $key => $val) {
209 3
            if (is_callable($item)) {
210 1
                if ($item($val, $key, $index)) {
211 1
                    return $index;
212
                }
213 1
            } else {
214 2
                if ($item === $val) {
215 1
                    return $index;
216
                }
217
            }
218 3
            $index++;
219 3
        }
220
221 1
        throw new RuntimeException("Could not find item equal to '{$item}'");
222
    }
223
224
    /**
225
     * Get item by key
226
     *
227
     * Fetches an item from the collection by key. If no item is found with the given key, a default may be provided as
228
     * the second argument. If no default is provided, null will be returned instead.
229
     *
230
     * @param mixed $key The key of the item you want returned
231
     * @param mixed $default A default value to return if key does not exist
232
     *
233
     * @return mixed
234
     */
235 14
    public function get($key, $default = null)
236
    {
237 14
        if ($this->has($key)) {
238 14
            return $this->items[$key];
239
        }
240
241 3
        return $default;
242
    }
243
244
    /**
245
     * Add an item with no regard to key
246
     *
247
     * Simply adds a value at the end of the collection. A numeric index will be created automatically.
248
     *
249
     * @param mixed $value The value to add to the collection
250
     *
251
     * @return self
252
     */
253 9
    public function add($value)
254
    {
255 9
        $this->items[] = $value;
256
257 9
        return $this;
258
    }
259
260
    /**
261
     * Assign a value to the given key
262
     *
263
     * Sets the specified key to the specified value. By default the key will be overwritten if it already exists, but
264
     * this behavior may be changed by setting the third parameter ($overwrite) to false.
265
     *
266
     * @param mixed $key The key to assign a value to
267
     * @param mixed $value The value to assign to $key
268
     * @param bool $overwrite Whether to overwrite existing values (default is true)
269
     *
270
     * @return self
271
     */
272 24
    public function set($key, $value, $overwrite = true)
273
    {
274 24
        if ($overwrite || !$this->has($key)) {
275 24
            $this->items[$key] = $value;
276 24
        }
277
278 24
        return $this;
279
    }
280
281
    /**
282
     * Delete an item by key
283
     *
284
     * Remove the item at the given key from the collection.
285
     *
286
     * @param mixed $key The key of the item to remove
287
     *
288
     * @return self
289
     */
290 3
    public function delete($key)
291
    {
292 3
        unset($this->items[$key]);
293
294 3
        return $this;
295
    }
296
297
    /**
298
     * Clear (remove) all items from the collection.
299
     *
300
     * @return self
301
     */
302 2
    public function clear()
303
    {
304 2
        $this->items = [];
305
306 2
        return $this;
307
    }
308
309
    /**
310
     * Determine if collection contains given value
311
     *
312
     * Checks the collection for an item exactly equal to $value. If $value is a callback function, it will be passed
313
     * the typical arguments ($value, $key, $index) and a true return value will count as a match.
314
     *
315
     * If $key argument is provided, key must match it as well. By default key is not required.
316
     *
317
     * @param mixed|callable $value The value to check for or a callback function
318
     * @param mixed $key The key to check for in addition to the value (optional)
319
     *
320
     * @return bool
321
     */
322 4
    public function contains($value, $key = null)
323
    {
324 4
        $index = 0;
325 4
        foreach ($this as $k => $v) {
326 4
            $matchkey = is_null($key) || $key === $k;
327 4
            if (is_callable($value)) {
328 1
                if ($value($v, $k, $index)) {
329 1
                    return $matchkey;
330
                }
331 1
            } else {
332 3
                if ($value === $v) {
333 3
                    return $matchkey;
334
                }
335
            }
336 4
            $index++;
337 4
        }
338 2
        return false;
339
    }
340
341
    /**
342
     * Pull an item out of the collection and return it
343
     *
344
     * @param mixed $key The key whose value should be removed and returned
345
     *
346
     * @return mixed
347
     */
348 1
    public function pull($key)
349
    {
350 1
        if ($this->has($key)) {
351 1
            $value = $this->get($key);
352 1
            $this->delete($key);
353 1
            return $value;
354
        }
355 1
    }
356
357
    /**
358
     * Join collection items using a delimiter
359
     *
360
     * Similar to implode() or join(), this method will attempt to return every item in the collection  delimited
361
     * (separated) by the specified character(s).
362
     *
363
     * @param string $delim The character(s) to delimit (separate) the results with
364
     *
365
     * @return string
366
     */
367 1
    public function join($delim = '')
368
    {
369 1
        return implode($delim, $this->items);
370
    }
371
372
    /**
373
     * Determine if collection is empty (has no items)
374
     *
375
     * @return bool
376
     */
377 8
    public function isEmpty()
378
    {
379 8
        return $this->count() == 0;
380
    }
381
382
    /**
383
     * Get new collection with only values
384
     *
385
     * Return a new collection with only the current collection's values. The keys will be indexed numerically from zero
386
     *
387
     * @return Collection
388
     */
389 3
    public function values()
390
    {
391 3
        return static::factory(array_values($this->items));
392
    }
393
394
    /**
395
     * Get new collection with only keys
396
     *
397
     * Return a new collection with only the current collection's keys as its values.
398
     *
399
     * @return Collection
400
     */
401 4
    public function keys()
402
    {
403 4
        return static::factory(array_keys($this->items));
404
    }
405
406
    /**
407
     * Get a collection of key/value pairs
408
     *
409
     * Returns a new collection containing arrays of key/value pairs in the format [key, value].
410
     *
411
     * @return Collection
412
     */
413 1
    public function pairs()
414
    {
415
        return $this->map(function($val, $key) {
416 1
            return [$key, $val];
417 1
        })->values();
418
    }
419
420
    /**
421
     * Get a collection with order reversed
422
     *
423
     * @return Collection
424
     */
425 6
    public function reverse()
426
    {
427 6
        return static::factory(array_reverse($this->items));
428
    }
429
430
    /**
431
     * Get a collection with keys and values flipped.
432
     *
433
     * Returns a new collection containing the keys as values and the values as keys.
434
     *
435
     * @return Collection
436
     */
437 1
    public function flip()
438
    {
439 1
        $collection = static::factory();
440 1
        foreach ($this as $key => $val) {
441 1
            $collection->set($val, $key);
442 1
        }
443 1
        return $collection;
444
    }
445
446
    /**
447
     * Shuffle (randomize) the order of this collection's values (in-place)
448
     *
449
     * @return Collection
450
     */
451 1
    public function shuffle()
452
    {
453 1
        shuffle($this->items);
454 1
        return $this;
455
    }
456
457
    /**
458
     * Get a random value from the collection
459
     * 
460
     * @return mixed
461
     */
462 1
    public function random()
463
    {
464 1
        return $this->getValueAt(rand(1, $this->count()));
465
    }
466
467
    /**
468
     * Sort the collection by value (in-place)
469
     *
470
     * Sorts the collection by value using the provided algorithm (which can be either the name of a native php function
471
     * or a callable).
472
     *
473
     * @note The sorting methods are exceptions to the usual callback signature. The callback for this method accepts
474
     *       the standard arguments for sorting algorithms ( string $str1 , string $str2 ) and should return an integer.
475
     *
476
     * @see http://php.net/manual/en/function.strcmp.php
477
     *
478
     * @param callable $alg The sorting algorithm (defaults to strcmp)
479
     *
480
     * @return self
481
     */
482 8
    public function sort(callable $alg = null)
483
    {
484 8
        if (is_null($alg)) {
485
            // case-sensitive string comparison is the default sorting mechanism
486 7
            $alg = 'strcmp';
487 7
        }
488 8
        uasort($this->items, $alg);
489
490 8
        return $this;
491
    }
492
493
    /**
494
     * Sort the collection by key (in-place)
495
     *
496
     * Sorts the collection by key using the provided algorithm (which can be either the name of a native php function
497
     * or a callable).
498
     *
499
     * @note The sorting methods are exceptions to the usual callback signature. The callback for this method accepts
500
     *       the standard arguments for sorting algorithms ( string $str1 , string $str2 ) and should return an integer.
501
     *
502
     * @see http://php.net/manual/en/function.strcmp.php
503
     *
504
     * @param callable $alg The sorting algorithm (defaults to strcmp)
505
     *
506
     * @return self
507
     */
508 2
    public function ksort(callable $alg = null)
509
    {
510 2
        if (is_null($alg)) {
511
            // case-sensitive string comparison is the default sorting mechanism
512 1
            $alg = 'strcmp';
513 1
        }
514 2
        uksort($this->items, $alg);
515
516 2
        return $this;
517
    }
518
519
    /**
520
     * Append items to collection without regard to key
521
     *
522
     * Much like Collection::add(), except that it accepts multiple items to append rather than just one.
523
     *
524
     * @param array|Traversable $items A list of values to append to the collection
525
     *
526
     * @return self
527
     */
528 2
    public function append($items)
529
    {
530 2
        if (!is_traversable($items)) {
531 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
532
        }
533
534 1
        foreach ($items as $val) {
535 1
            $this->add($val);
536 1
        }
537
538 1
        return $this;
539
    }
540
541
    /**
542
     * Return first item or first item where callback returns true
543
     *
544
     * Returns the first item in the collection. If a callback is provided, it will accept the standard arguments
545
     * ($value, $key, $index) and returning true will be considered a "match".
546
     *
547
     * @param callable|null $callback A callback to compare items with (optional)
548
     *
549
     * @return mixed|null
550
     */
551 8
    public function first(callable $callback = null)
552
    {
553 8
        $index = 0;
554 8
        foreach ($this as $key => $val) {
555 8
            if (is_null($callback) || $callback($val, $key, $index++)) {
556 8
                return $val;
557
            }
558 6
        }
559
560
        return null;
561
    }
562
563
    /**
564
     * Return last item or last item where callback returns true
565
     *
566
     * Returns the last item in the collection. If a callback is provided, it will accept the standard arguments
567
     * ($value, $key, $index) and returning true will be considered a "match".
568
     *
569
     * @param callable|null $callback A callback to compare items with (optional)
570
     *
571
     * @return mixed|null
572
     */
573 3
    public function last(callable $callback = null)
574
    {
575 3
        return $this->reverse()->first($callback);
576
    }
577
578
    /**
579
     * Create a new collection by applying a callback to each item in the collection
580
     *
581
     * The callback for this method should accept the standard arguments ($value, $key, $index). It will be called once
582
     * for every item in the collection and a new collection will be created with the results.
583
     *
584
     * @note It is worth noting that keys will be preserved in the resulting collection, so if you do not want this
585
     *       behavior, simply call values() on the resulting collection and it will be indexed numerically.
586
     *
587
     * @param callable $callback A callback that is applied to every item in the collection
588
     *
589
     * @return Collection
590
     */
591 4
    public function map(callable $callback)
592
    {
593 4
        $collection = static::factory();
594
595 4
        $index = 0;
596 4
        foreach ($this as $key => $val) {
597 4
            $collection->set($key, $callback($val, $key, $index++));
598 4
        }
599
600 4
        return $collection;
601
    }
602
603
    /**
604
     * Combine collection with another collection/array/traversable
605
     *
606
     * Using this collection's keys, and the incoming collection's values, a new collection is created and returned.
607
     *
608
     * @param array|Traversable $items The values to combine with this collection's keys
609
     *
610
     * @return Collection
611
     */
612 5
    public function combine($items)
613
    {
614 5
        if (!is_traversable($items)) {
615 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
616
        }
617
618 4
        $items = to_array($items);
619 4
        if (count($items) != count($this->items)) {
620 1
            throw new RuntimeException("Invalid input for " . __METHOD__ . ", number of items does not match");
621
        }
622
623 3
        return static::factory(array_combine($this->items, $items));
624
    }
625
626
    /**
627
     * Get a new collection with only distinct values
628
     *
629
     * @return Collection
630
     */
631 1
    public function distinct()
632
    {
633 1
        $collection = static::factory();
634 1
        foreach ($this as $key => $val) {
635 1
            if (!$collection->contains($val)) {
636 1
                $collection->set($key, $val);
637 1
            }
638 1
        }
639
640 1
        return $collection;
641
    }
642
643
    /**
644
     * Remove all duplicate values from collection (in-place)
645
     *
646
     * @return Collection
647
     */
648 1
    public function deduplicate()
649
    {
650 1
        $this->items = array_unique($this->items);
651
652 1
        return $this;
653
    }
654
655
    /**
656
     * Get frequency of each distinct item in collection
657
     *
658
     * Returns a new collection with each distinct scalar value converted to a string as its keys and the number if
659
     * times it occurs in the collection (its frequency) as its values. Non-scalar values will simply be discarded.
660
     *
661
     * @return Collection
662
     */
663 5
    public function frequency()
664
    {
665
        return $this->fold(function(Collection $freq, $val) {
666 4
            if (is_scalar($val)) {
667 4
                $str = (string) $val;
668 4
                if (!isset($freq[$str])) {
669 4
                    $freq[$str] = 0;
670 4
                }
671 4
                $freq[$str] += 1;
672 4
            }
673 4
            return $freq;
674 5
        }, new Collection);
675
    }
676
677
    /**
678
     * Get new collection with only filtered values
679
     *
680
     * Loops through every item in the collection, applying the given callback and creating a new collection with only
681
     * those items which return true from the callback. The callback should accept the standard arguments
682
     * ($value, $key, $index). If no callback is provided, items with "truthy" values will be kept.
683
     *
684
     * @param callable $callback A callback function used to determine which items are kept (optional)
685
     *
686
     * @return Collection
687
     */
688 12
    public function filter(callable $callback = null)
689
    {
690 12
        $collection = static::factory();
691 12
        $index = 0;
692 12
        foreach ($this as $key => $value) {
693 9
            if (is_null($callback)) {
694 1
                if ($value) {
695 1
                    $collection->set($key, $value);
696 1
                }
697 1
            } else {
698 8
                if ($callback($value, $key, $index++)) {
699 8
                    $collection->set($key, $value);
700 8
                }
701
            }
702 12
        }
703
704 12
        return $collection;
705
    }
706
707
    /**
708
     * Fold collection into a single value (a.k.a. reduce)
709
     *
710
     * Apply a callback function to each item in the collection, passing the result to the next call until only a single
711
     * value remains. The arguments provided to this callback are ($folded, $val, $key, $index) where $folded is the
712
     * result of the previous call (or if the first call it is equal to the $initial param).
713
     *
714
     * @param callable $callback The callback function used to "fold" or "reduce" the collection into a single value
715
     * @param mixed $initial The (optional) initial value to pass to the callback
716
     *
717
     * @return mixed
718
     */
719 13
    public function fold(callable $callback, $initial = null)
720
    {
721 13
        $index = 0;
722 13
        $folded = $initial;
723 13
        foreach ($this as $key => $val) {
724 11
            $folded = $callback($folded, $val, $key, $index++);
725 13
        }
726
727 13
        return $folded;
728
    }
729
730
    /**
731
     * Return a merge of this collection and $items
732
     *
733
     * Returns a new collection with a merge of this collection and $items. Values from $items will overwrite values in
734
     * the current collection.
735
     *
736
     * @param array|Traversable $items The items to merge with the collection
737
     *
738
     * @return Collection
739
     */
740 3
    public function merge($items)
741
    {
742 3
        if (!is_traversable($items)) {
743 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
744
        }
745
746 2
        $collection = clone $this;
747 2
        foreach ($items as $key => $val) {
748 2
            $collection->set($key, $val);
749 2
        }
750
751 2
        return $collection;
752
    }
753
754
    /**
755
     * Create a new collection with a union of this collection and $items
756
     *
757
     * This method is similar to merge, except that existing values will not be overwritten.
758
     *
759
     * @param $items
760
     */
761 2
    public function union($items)
762
    {
763 2
        if (!is_traversable($items)) {
764 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
765
        }
766
767 1
        $collection = clone $this;
768 1
        foreach ($items as $key => $val) {
769 1
            $collection->set($key, $val, false);
770 1
        }
771
772 1
        return $collection;
773
    }
774
775
    /**
776
     * Apply a callback function to each item in the collection passively
777
     *
778
     * To stop looping through the items in the collection, return false from the callback.
779
     *
780
     * @param callable $callback The callback to use on each item in the collection
781
     *
782
     * @return self
783
     */
784 2
    public function each(callable $callback)
785
    {
786 2
        $index = 0;
787 2
        foreach ($this as $key => $val) {
788 2
            if ($callback($val, $key, $index++) === false) {
789 1
                break;
790
            }
791 2
        }
792
793 2
        return $this;
794
    }
795
796
    /**
797
     * Assert callback returns $expected value for each item in collection.
798
     *
799
     * This method will loop over each item in the collection, passing them to the callback. If the callback doesn't
800
     * return $expected value for every item in the collection, it will return false.
801
     *
802
     * @param callable $callback Assertion callback
803
     * @param bool $expected Expected value from callback
804
     *
805
     * @return bool
806
     */
807 2
    public function assert(callable $callback, $expected = true)
808
    {
809 2
        $index = 0;
810 2
        foreach ($this as $key => $val) {
811 2
            if ($callback($val, $key, $index++) !== $expected) {
812 2
                return false;
813
            }
814 2
        }
815
816 2
        return true;
817
    }
818
819
    /**
820
     * Pipe collection through a callback
821
     *
822
     * Simply passes the collection as an argument to the given callback.
823
     *
824
     * @param callable $callback The callback function (passed only one arg, the collection itself)
825
     *
826
     * @return mixed
827
     */
828 1
    public function pipe(callable $callback)
829
    {
830 1
        return $callback($this);
831
    }
832
833
    /**
834
     * Get new collection in chunks of $size
835
     *
836
     * Creates a new collection of arrays of $size length. The remainder items will be placed at the end.
837
     *
838
     * @param int $size The size of the arrays you want returned
839
     *
840
     * @return Collection
841
     */
842 2
    public function chunk($size)
843
    {
844 2
        return static::factory(array_chunk($this->items, $size, true));
845
    }
846
847
    /**
848
     * Get a new collection of $count chunks
849
     *
850
     * Returns a collection of $count number of equally-sized arrays, placing remainders at the end.
851
     *
852
     * @param int $count The number of arrays you want returned
853
     *
854
     * @return Collection
855
     */
856 1
    public function split($count = 1)
857
    {
858 1
        return $this->chunk(ceil($this->count() / $count));
859
    }
860
861
    /**
862
     * Get a slice of this collection.
863
     *
864
     * Returns a collection with a slice of this collection's items, starting at $offset and continuing until $length
865
     *
866
     * @param int $offset The offset at which you want the slice to begin
867
     * @param int|null $length The length of the slice (number of items)
868
     *
869
     * @return Collection
870
     */
871 1
    public function slice($offset, $length = null)
872
    {
873 1
        return static::factory(array_slice($this->items, $offset, $length, true));
874
    }
875
876
    /**
877
     * Zip together any number of arrays/traversables
878
     *
879
     * Merges together the values from this collection with the values of each of the provided traversables at the
880
     * corresponding index. So [1,2,3] + [4,5,6] + [7,8,9] would end up [[1,4,7], [2,5,8], [3,6,9]].
881
     *
882
     * @param array|Traversable ...$items The collections/arrays to zip
883
     *
884
     * @return Collection
885
     */
886 1
    public function zip(...$items)
887
    {
888 1
        $args = [null, $this->items];
889 1
        foreach ($items as $x) {
890 1
            $args[] = to_array($x);
891 1
        }
892 1
        return static::factory(call_user_func_array('array_map', $args));
893
    }
894
895
    /**
896
     * Get every n-th item from the collection
897
     *
898
     * @param int $n Get every $n-th item
899
     */
900 1
    public function nth($n)
901
    {
902
        return $this->filter(function($val, $key, $index) use ($n) {
903 1
            return ($index+1) % $n == 0;
904 1
        });
905
    }
906
907
    /**
908
     * Get collection with only differing items
909
     *
910
     * Returns a collection containing only the items not present in *both* this collection and $items.
911
     *
912
     * @param array|Traversable $items The items to compare with
913
     *
914
     * @return Collection
915
     */
916 1
    public function diff($items)
917
    {
918 1
        return static::factory(array_diff($this->items, to_array($items)));
919
    }
920
921
    /**
922
     * Get collection with only differing items (by key)
923
     *
924
     * Returns a collection containing only the values whose keys are not present in *both* this collection and $items.
925
     *
926
     * @param array|Traversable $items The items to compare with
927
     *
928
     * @return Collection
929
     */
930 2
    public function kdiff($items)
931
    {
932 2
        return static::factory(array_diff_key($this->items, to_array($items)));
933
    }
934
935
    /**
936
     * Get collection with only intersecting items
937
     *
938
     * Returns a collection containing only the values present in *both* this collection and $items
939
     *
940
     * @param array|Traversable $items The items to compare with
941
     *
942
     * @return Collection
943
     */
944 1
    public function intersect($items)
945
    {
946 1
        return static::factory(array_intersect($this->items, to_array($items)));
947
    }
948
949
    /**
950
     * Get collection with only intersecting items (by key)
951
     *
952
     * Returns a collection containing only the values whose keys are present in *both* this collection and $items
953
     *
954
     * @param array|Traversable $items The items to compare with
955
     *
956
     * @return Collection
957
     */
958 1
    public function kintersect($items)
959
    {
960 1
        return static::factory(array_intersect_key($this->items, to_array($items)));
961
    }
962
963
    /**
964
     * Remove last item in collection and return it
965
     *
966
     * @return mixed
967
     */
968 4
    public function pop()
969
    {
970 4
        return array_pop($this->items);
971
    }
972
973
    /**
974
     * Remove first item in collection and return it
975
     *
976
     * If the collection is numerically indexed, this method will re-index it from 0 after returning the item.
977
     *
978
     * @return mixed
979
     */
980 2
    public function shift()
981
    {
982 2
        return array_shift($this->items);
983
    }
984
985
    /**
986
     * Add item to the end of the collection
987
     *
988
     * @note This method is no different than add() but I included it for consistency's sake since I have the others
989
     *
990
     * @param mixed $item The item to add to the collection
991
     *
992
     * @return self
993
     */
994 1
    public function push($item)
995
    {
996 1
        return $this->add($item);
997
    }
998
999
    /**
1000
     * Add item to the beginning of the collection
1001
     *
1002
     * The collection will be re-indexed if it has numeric keys.
1003
     *
1004
     * @param mixed $item The item to add to the collection
1005
     *
1006
     * @return self
1007
     */
1008 5
    public function unshift($item)
1009
    {
1010 5
        array_unshift($this->items, $item);
1011
1012 5
        return $this;
1013
    }
1014
1015
    /**
1016
     * Get new collection padded to specified $size with $value
1017
     *
1018
     * Using $value, pad the collection to specified $size. If $size is smaller or equal to the size of the collection,
1019
     * then no padding takes place. If $size is positive, padding is added to the end, while if negative, padding will
1020
     * be added to the beginning.
1021
     *
1022
     * @param int $size The number of items collection should have
1023
     * @param mixed $value The value to pad with
1024
     *
1025
     * @return Collection
1026
     */
1027 3
    public function pad($size, $value = null)
1028
    {
1029 3
        $collection = clone $this;
1030 3
        while ($collection->count() < abs($size)) {
1031 3
            if ($size > 0) {
1032 3
                $collection->add($value);
1033 3
            } else {
1034 3
                $collection->unshift($value);
1035
            }
1036 3
        }
1037
1038 3
        return $collection;
1039
    }
1040
1041
    /**
1042
     * Partition collection into two collections using a callback
1043
     *
1044
     * Iterates over each element in the collection with a callback. Items where callback returns true are placed in one
1045
     * collection and the rest in another. Finally, the two collections are placed in an array and returned for easy use
1046
     * with the list() function. ( `list($a, $b) = $col->partition(function($val, $key, $index) {})` )
1047
     *
1048
     * @param callable $callback The comparison callback
1049
     *
1050
     * @return Collection[]
1051
     */
1052 2
    public function partition(callable $callback)
1053
    {
1054 2
        $pass = static::factory();
1055 2
        $fail = static::factory();
1056
1057 2
        $index = 0;
1058 2
        foreach ($this as $key => $val) {
1059 1
            if ($callback($val, $key, $index++)) {
1060 1
                $pass->set($key, $val);
1061 1
            } else {
1062 1
                $fail->set($key, $val);
1063
            }
1064 2
        }
1065
1066 2
        return [$pass, $fail];
1067
    }
1068
1069
    /**
1070
     * Get sum of all numeric items
1071
     *
1072
     * Returns the sum of all numeric items in the collection, silently ignoring any non-numeric values.
1073
     *
1074
     * @return float|int
1075
     */
1076 5
    public function sum()
1077
    {
1078
        return $this->fold(function($accum, $val) {
1079 4
            return is_numeric($val) ? $accum + $val : $accum;
1080 5
        }, 0);
1081
    }
1082
1083
    /**
1084
     * Get product of all numeric items
1085
     *
1086
     * Returns the product of all numeric items in the collection, silently ignoring any non-numeric values.
1087
     *
1088
     * @return float|int
1089
     */
1090 3
    public function product()
1091
    {
1092 3
        if ($this->isEmpty()) {
1093 1
            return 0;
1094
        }
1095
        return $this->fold(function($accum, $val) {
1096 2
            return is_numeric($val) ? $accum * $val : $accum;
1097 2
        }, 1);
1098
    }
1099
1100
    /**
1101
     * Get average of all numeric items
1102
     *
1103
     * Returns the average of all numeric items in the collection, silently ignoring any non-numeric values.
1104
     *
1105
     * @return float|int
1106
     */
1107 3
    public function average()
1108
    {
1109 3
        $numeric = $this->filter('Noz\is_numeric');
1110 3
        if (!$count = $numeric->count()) {
1111 1
            return 0;
1112
        }
1113 2
        return $numeric->sum() / $count;
1114
    }
1115
1116
    /**
1117
     * Get the median numeric value
1118
     *
1119
     * Returns the median of all numeric items in the collection, silently ignoring any non-numeric values.
1120
     *
1121
     * @return float|int
1122
     */
1123 3
    public function median()
1124
    {
1125 3
        $numeric = $this->filter('Noz\is_numeric')->sort();
1126 3
        if (!$count = $numeric->count()) {
1127 1
            return 0;
1128
        }
1129 2
        $pos = ($count + 1) / 2;
1130 2
        if (!is_int($pos)) {
1131 2
            return ($numeric->getValueAt(floor($pos)) + $numeric->getValueAt(ceil($pos))) / 2;
1132
        }
1133 1
        return to_numeric($numeric->getValueAt($pos));
1134
    }
1135
1136
    /**
1137
     * Get the mode numeric value
1138
     *
1139
     * Returns the mode of all numeric items in the collection, silently ignoring any non-numeric values.
1140
     *
1141
     * @return float|int
1142
     */
1143 3
    public function mode()
1144
    {
1145 3
        $mode = $this->filter('Noz\is_numeric')
1146 3
            ->frequency()
1147 3
            ->sort()
1148 3
            ->keys()
1149 3
            ->pop();
1150
1151 3
        return to_numeric($mode);
1152
    }
1153
1154
    /**
1155
     * Get maximum numeric value from collection
1156
     *
1157
     * Returns the max of all numeric items in the collection, silently ignoring any non-numeric values.
1158
     *
1159
     * @return float|int
1160
     */
1161 1
    public function max()
1162
    {
1163 1
        return to_numeric(max($this->items));
1164
    }
1165
1166
    /**
1167
     * Get minimum numeric value from collection
1168
     *
1169
     * Returns the min of all numeric items in the collection, silently ignoring any non-numeric values.
1170
     *
1171
     * @return float|int
1172
     */
1173 1
    public function min()
1174
    {
1175 1
        return to_numeric(min($this->items));
1176
    }
1177
1178
    /**
1179
     * Get column values by key
1180
     *
1181
     * This method expects the collection's data to be tabular in nature (two-dimensional and for the rows to have
1182
     * consistently named keys). If the data is not structured this way, it will do the best it can but it is not meant
1183
     * for unstructured, non-tabular data so don't expect consistent results.
1184
     *
1185
     * @param string|int $column The key of the column you want to get
1186
     *
1187
     * @return Collection
1188
     */
1189 3
    public function getColumn($column)
1190
    {
1191 3
        return static::factory(array_column($this->items, $column));
1192
    }
1193
1194
    /**
1195
     * Is collection tabular?
1196
     *
1197
     * Returns true if the data in the collection is tabular in nature, meaning it is at least two-dimensional and each
1198
     * row contains the same number of values with the same keys.
1199
     *
1200
     * @return bool
1201
     */
1202 1
    public function isTabular()
1203
    {
1204 1
        $first = $this->first();
1205 1
        return $this->assert(function($row) use ($first) {
1206 1
            if (!is_traversable(($first)) || !is_traversable($row)) {
1207 1
                return false;
1208
            }
1209 1
            return Collection::factory($row)
1210 1
                ->kdiff($first)
1211 1
                ->isEmpty();
1212 1
        });
1213
    }
1214
1215
    /** ++++                  ++++ **/
1216
    /** ++ Interface Compliance ++ **/
1217
    /** ++++                  ++++ **/
1218
1219
    /**
1220
     * JSON serialize
1221
     *
1222
     * @ignore
1223
     *
1224
     * @return array
1225
     */
1226 1
    public function jsonSerialize()
1227
    {
1228 1
        return $this->toArray();
1229
    }
1230
1231
    /** ++++                  ++++ **/
1232
    /** ++ Array Access Methods ++ **/
1233
    /** ++++                  ++++ **/
1234
1235
    /**
1236
     * Does offset exist?
1237
     *
1238
     * @ignore
1239
     *
1240
     * @param mixed $offset
1241
     *
1242
     * @return bool
1243
     */
1244 5
    public function offsetExists($offset)
1245
    {
1246 5
        return $this->has($offset);
1247
    }
1248
1249
    /**
1250
     * Get item at offset
1251
     *
1252
     * @ignore
1253
     *
1254
     * @param mixed $offset
1255
     *
1256
     * @return mixed
1257
     */
1258 6
    public function offsetGet($offset)
1259
    {
1260 6
        if (!$this->has($offset)) {
1261 1
            throw new RuntimeException("Unknown offset: {$offset}");
1262
        }
1263
1264 5
        return $this->get($offset);
1265
    }
1266
1267
    /**
1268
     * Unset item at offset
1269
     *
1270
     * @ignore
1271
     *
1272
     * @param mixed $offset
1273
     *
1274
     * @return void
1275
     */
1276 1
    public function offsetUnset($offset)
1277
    {
1278 1
        $this->delete($offset);
1279 1
    }
1280
1281
    /**
1282
     * Set item at offset
1283
     *
1284
     * @ignore
1285
     *
1286
     * @param mixed $offset
1287
     * @param mixed $value
1288
     *
1289
     * @return self
1290
     */
1291 5
    public function offsetSet($offset, $value)
1292
    {
1293 5
        if (!isset($offset)) {
1294 1
            $this->add($value);
1295 1
        }
1296
1297 5
        $this->set($offset, $value);
1298 5
    }
1299
1300
    /** ++++                  ++++ **/
1301
    /** ++   Iterator Methods   ++ **/
1302
    /** ++++                  ++++ **/
1303
1304
    /**
1305
     * @ignore
1306
     */
1307 52
    public function current()
1308
    {
1309 52
        return current($this->items);
1310
    }
1311
1312
    /**
1313
     * @ignore
1314
     */
1315 52
    public function key()
1316
    {
1317 52
        return key($this->items);
1318
    }
1319
1320
    /**
1321
     * @ignore
1322
     */
1323 51
    public function next()
1324
    {
1325 51
        return next($this->items);
1326
    }
1327
1328
    /**
1329
     * @ignore
1330
     */
1331 122
    public function rewind()
1332
    {
1333 122
        reset($this->items);
1334 122
    }
1335
1336
    /**
1337
     * @ignore
1338
     */
1339 57
    public function valid()
1340
    {
1341 57
        return $this->has(key($this->items));
1342
    }
1343
1344
    /** ++++                  ++++ **/
1345
    /** ++   Countable Method   ++ **/
1346
    /** ++++                  ++++ **/
1347
1348
    /**
1349
     * Get number of items in the collection
1350
     *
1351
     * @return int
1352
     */
1353 20
    public function count()
1354
    {
1355 20
        return count($this->items);
1356
    }
1357
}