Completed
Pull Request — master (#19)
by Luke
06:21
created

Collection::sum()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 1
nop 0
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 2
rs 10
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 121
    public function __construct(array $items = [])
52
    {
53 121
        $this->items = $items;
54 121
        $this->rewind();
55 121
    }
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 48
    public static function factory($items = null)
69
    {
70 48
        return new Collection(to_array($items, true));
71
    }
72
73
    /**
74
     * Get collection as an array
75
     *
76
     * @return array
77
     */
78 54
    public function toArray()
79
    {
80 54
        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 67
    public function has($key)
91
    {
92 67
        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 23
    public function set($key, $value, $overwrite = true)
273
    {
274 23
        if ($overwrite || !$this->has($key)) {
275 23
            $this->items[$key] = $value;
276 23
        }
277
278 23
        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 2
    public function values()
390
    {
391 2
        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 11
    public function filter(callable $callback = null)
689
    {
690 11
        $collection = static::factory();
691 11
        $index = 0;
692 11
        foreach ($this as $key => $value) {
693 8
            if (is_null($callback)) {
694 1
                if ($value) {
695 1
                    $collection->set($key, $value);
696 1
                }
697 1
            } else {
698 7
                if ($callback($value, $key, $index++)) {
699 7
                    $collection->set($key, $value);
700 7
                }
701
            }
702 11
        }
703
704 11
        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 collection with only differing items
897
     *
898
     * Returns a collection containing only the items not present in *both* this collection and $items.
899
     *
900
     * @param array|Traversable $items The items to compare with
901
     *
902
     * @return Collection
903
     */
904 1
    public function diff($items)
905
    {
906 1
        return static::factory(array_diff($this->items, to_array($items)));
907
    }
908
909
    /**
910
     * Get collection with only differing items (by key)
911
     *
912
     * Returns a collection containing only the values whose keys are not present in *both* this collection and $items.
913
     *
914
     * @param array|Traversable $items The items to compare with
915
     *
916
     * @return Collection
917
     */
918 2
    public function kdiff($items)
919
    {
920 2
        return static::factory(array_diff_key($this->items, to_array($items)));
921
    }
922
923
    /**
924
     * Get collection with only intersecting items
925
     *
926
     * Returns a collection containing only the values present in *both* this collection and $items
927
     *
928
     * @param array|Traversable $items The items to compare with
929
     *
930
     * @return Collection
931
     */
932 1
    public function intersect($items)
933
    {
934 1
        return static::factory(array_intersect($this->items, to_array($items)));
935
    }
936
937
    /**
938
     * Get collection with only intersecting items (by key)
939
     *
940
     * Returns a collection containing only the values whose keys are present in *both* this collection and $items
941
     *
942
     * @param array|Traversable $items The items to compare with
943
     *
944
     * @return Collection
945
     */
946 1
    public function kintersect($items)
947
    {
948 1
        return static::factory(array_intersect_key($this->items, to_array($items)));
949
    }
950
951
    /**
952
     * Remove last item in collection and return it
953
     *
954
     * @return mixed
955
     */
956 4
    public function pop()
957
    {
958 4
        return array_pop($this->items);
959
    }
960
961
    /**
962
     * Remove first item in collection and return it
963
     *
964
     * If the collection is numerically indexed, this method will re-index it from 0 after returning the item.
965
     *
966
     * @return mixed
967
     */
968 2
    public function shift()
969
    {
970 2
        return array_shift($this->items);
971
    }
972
973
    /**
974
     * Add item to the end of the collection
975
     *
976
     * @note This method is no different than add() but I included it for consistency's sake since I have the others
977
     *
978
     * @param mixed $item The item to add to the collection
979
     *
980
     * @return self
981
     */
982 1
    public function push($item)
983
    {
984 1
        return $this->add($item);
985
    }
986
987
    /**
988
     * Add item to the beginning of the collection
989
     *
990
     * The collection will be re-indexed if it has numeric keys.
991
     *
992
     * @param mixed $item The item to add to the collection
993
     *
994
     * @return self
995
     */
996 5
    public function unshift($item)
997
    {
998 5
        array_unshift($this->items, $item);
999
1000 5
        return $this;
1001
    }
1002
1003
    /**
1004
     * Get new collection padded to specified $size with $value
1005
     *
1006
     * Using $value, pad the collection to specified $size. If $size is smaller or equal to the size of the collection,
1007
     * then no padding takes place. If $size is positive, padding is added to the end, while if negative, padding will
1008
     * be added to the beginning.
1009
     *
1010
     * @param int $size The number of items collection should have
1011
     * @param mixed $value The value to pad with
1012
     *
1013
     * @return Collection
1014
     */
1015 3
    public function pad($size, $value = null)
1016
    {
1017 3
        $collection = clone $this;
1018 3
        while ($collection->count() < abs($size)) {
1019 3
            if ($size > 0) {
1020 3
                $collection->add($value);
1021 3
            } else {
1022 3
                $collection->unshift($value);
1023
            }
1024 3
        }
1025
1026 3
        return $collection;
1027
    }
1028
1029
    /**
1030
     * Partition collection into two collections using a callback
1031
     *
1032
     * Iterates over each element in the collection with a callback. Items where callback returns true are placed in one
1033
     * collection and the rest in another. Finally, the two collections are placed in an array and returned for easy use
1034
     * with the list() function. ( `list($a, $b) = $col->partition(function($val, $key, $index) {})` )
1035
     *
1036
     * @param callable $callback The comparison callback
1037
     *
1038
     * @return Collection[]
1039
     */
1040 2
    public function partition(callable $callback)
1041
    {
1042 2
        $pass = static::factory();
1043 2
        $fail = static::factory();
1044
1045 2
        $index = 0;
1046 2
        foreach ($this as $key => $val) {
1047 1
            if ($callback($val, $key, $index++)) {
1048 1
                $pass->set($key, $val);
1049 1
            } else {
1050 1
                $fail->set($key, $val);
1051
            }
1052 2
        }
1053
1054 2
        return [$pass, $fail];
1055
    }
1056
1057
    /**
1058
     * Get sum of all numeric items
1059
     *
1060
     * Returns the sum of all numeric items in the collection, silently ignoring any non-numeric values.
1061
     *
1062
     * @return float|int
1063
     */
1064 5
    public function sum()
1065
    {
1066
        return $this->fold(function($accum, $val) {
1067 4
            return is_numeric($val) ? $accum + $val : $accum;
1068 5
        }, 0);
1069
    }
1070
1071
    /**
1072
     * Get product of all numeric items
1073
     *
1074
     * Returns the product of all numeric items in the collection, silently ignoring any non-numeric values.
1075
     *
1076
     * @return float|int
1077
     */
1078 3
    public function product()
1079
    {
1080 3
        if ($this->isEmpty()) {
1081 1
            return 0;
1082
        }
1083
        return $this->fold(function($accum, $val) {
1084 2
            return is_numeric($val) ? $accum * $val : $accum;
1085 2
        }, 1);
1086
    }
1087
1088
    /**
1089
     * Get average of all numeric items
1090
     *
1091
     * Returns the average of all numeric items in the collection, silently ignoring any non-numeric values.
1092
     *
1093
     * @return float|int
1094
     */
1095 3
    public function average()
1096
    {
1097 3
        $numeric = $this->filter('Noz\is_numeric');
1098 3
        if (!$count = $numeric->count()) {
1099 1
            return 0;
1100
        }
1101 2
        return $numeric->sum() / $count;
1102
    }
1103
1104
    /**
1105
     * Get the median numeric value
1106
     *
1107
     * Returns the median of all numeric items in the collection, silently ignoring any non-numeric values.
1108
     *
1109
     * @return float|int
1110
     */
1111 3
    public function median()
1112
    {
1113 3
        $numeric = $this->filter('Noz\is_numeric')->sort();
1114 3
        if (!$count = $numeric->count()) {
1115 1
            return 0;
1116
        }
1117 2
        $pos = ($count + 1) / 2;
1118 2
        if (!is_int($pos)) {
1119 2
            return ($numeric->getValueAt(floor($pos)) + $numeric->getValueAt(ceil($pos))) / 2;
1120
        }
1121 1
        return to_numeric($numeric->getValueAt($pos));
1122
    }
1123
1124
    /**
1125
     * Get the mode numeric value
1126
     *
1127
     * Returns the mode of all numeric items in the collection, silently ignoring any non-numeric values.
1128
     *
1129
     * @return float|int
1130
     */
1131 3
    public function mode()
1132
    {
1133 3
        $mode = $this->filter('Noz\is_numeric')
1134 3
            ->frequency()
1135 3
            ->sort()
1136 3
            ->keys()
1137 3
            ->pop();
1138
1139 3
        return to_numeric($mode);
1140
    }
1141
1142
    /**
1143
     * Get maximum numeric value from collection
1144
     *
1145
     * Returns the max of all numeric items in the collection, silently ignoring any non-numeric values.
1146
     *
1147
     * @return float|int
1148
     */
1149 1
    public function max()
1150
    {
1151 1
        return to_numeric(max($this->items));
1152
    }
1153
1154
    /**
1155
     * Get minimum numeric value from collection
1156
     *
1157
     * Returns the min of all numeric items in the collection, silently ignoring any non-numeric values.
1158
     *
1159
     * @return float|int
1160
     */
1161 1
    public function min()
1162
    {
1163 1
        return to_numeric(min($this->items));
1164
    }
1165
1166
    /**
1167
     * Get column values by key
1168
     *
1169
     * This method expects the collection's data to be tabular in nature (two-dimensional and for the rows to have
1170
     * consistently named keys). If the data is not structured this way, it will do the best it can but it is not meant
1171
     * for unstructured, non-tabular data so don't expect consistent results.
1172
     *
1173
     * @param string|int $column The key of the column you want to get
1174
     *
1175
     * @return Collection
1176
     */
1177 3
    public function getColumn($column)
1178
    {
1179 3
        return static::factory(array_column($this->items, $column));
1180
    }
1181
1182
    /**
1183
     * Is collection tabular?
1184
     *
1185
     * Returns true if the data in the collection is tabular in nature, meaning it is at least two-dimensional and each
1186
     * row contains the same number of values with the same keys.
1187
     *
1188
     * @return bool
1189
     */
1190 1
    public function isTabular()
1191
    {
1192 1
        $first = $this->first();
1193 1
        return $this->assert(function($row) use ($first) {
1194 1
            if (!is_traversable(($first)) || !is_traversable($row)) {
1195 1
                return false;
1196
            }
1197 1
            return Collection::factory($row)
1198 1
                ->kdiff($first)
1199 1
                ->isEmpty();
1200 1
        });
1201
    }
1202
1203
    /** ++++                  ++++ **/
1204
    /** ++ Interface Compliance ++ **/
1205
    /** ++++                  ++++ **/
1206
1207
    /**
1208
     * JSON serialize
1209
     *
1210
     * @ignore
1211
     *
1212
     * @return array
1213
     */
1214 1
    public function jsonSerialize()
1215
    {
1216 1
        return $this->toArray();
1217
    }
1218
1219
    /** ++++                  ++++ **/
1220
    /** ++ Array Access Methods ++ **/
1221
    /** ++++                  ++++ **/
1222
1223
    /**
1224
     * Does offset exist?
1225
     *
1226
     * @ignore
1227
     *
1228
     * @param mixed $offset
1229
     *
1230
     * @return bool
1231
     */
1232 5
    public function offsetExists($offset)
1233
    {
1234 5
        return $this->has($offset);
1235
    }
1236
1237
    /**
1238
     * Get item at offset
1239
     *
1240
     * @ignore
1241
     *
1242
     * @param mixed $offset
1243
     *
1244
     * @return mixed
1245
     */
1246 6
    public function offsetGet($offset)
1247
    {
1248 6
        if (!$this->has($offset)) {
1249 1
            throw new RuntimeException("Unknown offset: {$offset}");
1250
        }
1251
1252 5
        return $this->get($offset);
1253
    }
1254
1255
    /**
1256
     * Unset item at offset
1257
     *
1258
     * @ignore
1259
     *
1260
     * @param mixed $offset
1261
     *
1262
     * @return void
1263
     */
1264 1
    public function offsetUnset($offset)
1265
    {
1266 1
        $this->delete($offset);
1267 1
    }
1268
1269
    /**
1270
     * Set item at offset
1271
     *
1272
     * @ignore
1273
     *
1274
     * @param mixed $offset
1275
     * @param mixed $value
1276
     *
1277
     * @return self
1278
     */
1279 5
    public function offsetSet($offset, $value)
1280
    {
1281 5
        if (!isset($offset)) {
1282 1
            $this->add($value);
1283 1
        }
1284
1285 5
        $this->set($offset, $value);
1286 5
    }
1287
1288
    /** ++++                  ++++ **/
1289
    /** ++   Iterator Methods   ++ **/
1290
    /** ++++                  ++++ **/
1291
1292
    /**
1293
     * @ignore
1294
     */
1295 51
    public function current()
1296
    {
1297 51
        return current($this->items);
1298
    }
1299
1300
    /**
1301
     * @ignore
1302
     */
1303 51
    public function key()
1304
    {
1305 51
        return key($this->items);
1306
    }
1307
1308
    /**
1309
     * @ignore
1310
     */
1311 50
    public function next()
1312
    {
1313 50
        return next($this->items);
1314
    }
1315
1316
    /**
1317
     * @ignore
1318
     */
1319 121
    public function rewind()
1320
    {
1321 121
        reset($this->items);
1322 121
    }
1323
1324
    /**
1325
     * @ignore
1326
     */
1327 56
    public function valid()
1328
    {
1329 56
        return $this->has(key($this->items));
1330
    }
1331
1332
    /** ++++                  ++++ **/
1333
    /** ++   Countable Method   ++ **/
1334
    /** ++++                  ++++ **/
1335
1336
    /**
1337
     * Get number of items in the collection
1338
     *
1339
     * @return int
1340
     */
1341 20
    public function count()
1342
    {
1343 20
        return count($this->items);
1344
    }
1345
}