Completed
Push — master ( 3a4e65...24e088 )
by Luke
16:49 queued 07:09
created

Collection::foldr()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
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 134
    public function __construct(array $items = [])
52
    {
53 134
        $this->items = $items;
54 134
        $this->rewind();
55 134
    }
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 59
    public static function factory($items = null)
69
    {
70 59
        return new Collection(to_array($items, true));
71
    }
72
73
    /**
74
     * Get collection as an array
75
     *
76
     * @return array
77
     */
78 63
    public function toArray()
79
    {
80 63
        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 74
    public function has($key)
91
    {
92 74
        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 10
    public function add($value)
254
    {
255 10
        $this->items[] = $value;
256
257 10
        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 7
    public function keys()
402
    {
403 7
        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 8
    public function reverse()
426
    {
427 8
        return static::factory(array_reverse($this->items, true));
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 2
    public function shuffle()
452
    {
453 2
        $new = [];
454 2
        $keys = array_keys($this->items);
455 2
        shuffle($keys);
456 2
        foreach ($keys as $key) {
457 2
            $new[$key] = $this->items[$key];
458 2
        }
459 2
        $this->items = $new;
460
461 2
        return $this;
462
    }
463
464
    /**
465
     * Get a random value from the collection
466
     * 
467
     * @return mixed
468
     */
469 1
    public function random()
470
    {
471 1
        return $this->getValueAt(rand(1, $this->count()));
472
    }
473
474
    /**
475
     * Sort the collection by value (in-place)
476
     *
477
     * Sorts the collection by value using the provided algorithm (which can be either the name of a native php function
478
     * or a callable).
479
     *
480
     * @note The sorting methods are exceptions to the usual callback signature. The callback for this method accepts
481
     *       the standard arguments for sorting algorithms ( string $str1 , string $str2 ) and should return an integer.
482
     *
483
     * @see http://php.net/manual/en/function.strcmp.php
484
     *
485
     * @param callable $alg The sorting algorithm (defaults to strcmp)
486
     *
487
     * @return self
488
     */
489 9
    public function sort(callable $alg = null)
490
    {
491 9
        if (is_null($alg)) {
492 8
            $flag = $this->assert('Noz\is_numeric') ? SORT_NUMERIC : SORT_NATURAL;
493 8
            asort($this->items, $flag);
494 8
        } else {
495 1
            uasort($this->items, $alg);
496
        }
497
498 9
        return $this;
499
    }
500
501
    /**
502
     * Sort the collection by key (in-place)
503
     *
504
     * Sorts the collection by key using the provided algorithm (which can be either the name of a native php function
505
     * or a callable).
506
     *
507
     * @note The sorting methods are exceptions to the usual callback signature. The callback for this method accepts
508
     *       the standard arguments for sorting algorithms ( string $str1 , string $str2 ) and should return an integer.
509
     *
510
     * @see http://php.net/manual/en/function.strcmp.php
511
     *
512
     * @param callable $alg The sorting algorithm (defaults to strcmp)
513
     *
514
     * @return self
515
     */
516 3
    public function ksort(callable $alg = null)
517
    {
518 3
        if (is_null($alg)) {
519 2
            $flag = $this->keys()->assert('Noz\is_numeric') ? SORT_NUMERIC : SORT_NATURAL;
520 2
            ksort($this->items, $flag);
521 2
        } else {
522 1
            uksort($this->items, $alg);
523
        }
524
525 3
        return $this;
526
    }
527
528
    /**
529
     * Append items to collection without regard to key
530
     *
531
     * Much like Collection::add(), except that it accepts multiple items to append rather than just one.
532
     *
533
     * @param array|Traversable $items A list of values to append to the collection
534
     *
535
     * @return self
536
     */
537 2
    public function append($items)
538
    {
539 2
        if (!is_traversable($items)) {
540 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
541
        }
542
543 1
        foreach ($items as $val) {
544 1
            $this->add($val);
545 1
        }
546
547 1
        return $this;
548
    }
549
550
    /**
551
     * Return first item or first item where callback returns true
552
     *
553
     * Returns the first item in the collection. If a callback is provided, it will accept the standard arguments
554
     * ($value, $key, $index) and returning true will be considered a "match".
555
     *
556
     * @param callable|null $callback A callback to compare items with (optional)
557
     *
558
     * @return mixed|null
559
     */
560 8
    public function first(callable $callback = null)
561
    {
562 8
        $index = 0;
563 8
        foreach ($this as $key => $val) {
564 8
            if (is_null($callback) || $callback($val, $key, $index++)) {
565 8
                return $val;
566
            }
567 6
        }
568
569
        return null;
570
    }
571
572
    /**
573
     * Return last item or last item where callback returns true
574
     *
575
     * Returns the last item in the collection. If a callback is provided, it will accept the standard arguments
576
     * ($value, $key, $index) and returning true will be considered a "match".
577
     *
578
     * @param callable|null $callback A callback to compare items with (optional)
579
     *
580
     * @return mixed|null
581
     */
582 3
    public function last(callable $callback = null)
583
    {
584 3
        return $this->reverse()->first($callback);
585
    }
586
587
    /**
588
     * Create a new collection by applying a callback to each item in the collection
589
     *
590
     * The callback for this method should accept the standard arguments ($value, $key, $index). It will be called once
591
     * for every item in the collection and a new collection will be created with the results.
592
     *
593
     * @note It is worth noting that keys will be preserved in the resulting collection, so if you do not want this
594
     *       behavior, simply call values() on the resulting collection and it will be indexed numerically.
595
     *
596
     * @param callable $callback A callback that is applied to every item in the collection
597
     *
598
     * @return Collection
599
     */
600 4
    public function map(callable $callback)
601
    {
602 4
        $collection = static::factory();
603
604 4
        $index = 0;
605 4
        foreach ($this as $key => $val) {
606 4
            $collection->set($key, $callback($val, $key, $index++));
607 4
        }
608
609 4
        return $collection;
610
    }
611
612
    /**
613
     * Combine collection with another collection/array/traversable
614
     *
615
     * Using this collection's keys, and the incoming collection's values, a new collection is created and returned.
616
     *
617
     * @param array|Traversable $items The values to combine with this collection's keys
618
     *
619
     * @return Collection
620
     */
621 5
    public function combine($items)
622
    {
623 5
        if (!is_traversable($items)) {
624 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
625
        }
626
627 4
        $items = to_array($items);
628 4
        if (count($items) != count($this->items)) {
629 1
            throw new RuntimeException("Invalid input for " . __METHOD__ . ", number of items does not match");
630
        }
631
632 3
        return static::factory(array_combine($this->items, $items));
633
    }
634
635
    /**
636
     * Create new collection with specified keys
637
     *
638
     * A new collection is created using this collection's values and the provided $keys (the opposite of combine)
639
     *
640
     * @param array|Traversable $keys A new set of keys
641
     *
642
     * @return Collection
643
     */
644 5
    public function rekey($keys)
645
    {
646 5
        if (!is_traversable($keys)) {
647 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
648
        }
649
650 4
        $keys = to_array($keys);
651 4
        if (count($keys) != count($this->items)) {
652 1
            throw new RuntimeException("Invalid input for " . __METHOD__ . ", number of items does not match");
653
        }
654
655 3
        return static::factory(array_combine($keys, $this->items));
656
    }
657
658
    /**
659
     * Get a new collection with only distinct values
660
     *
661
     * @return Collection
662
     */
663 1
    public function distinct()
664
    {
665 1
        $collection = static::factory();
666 1
        foreach ($this as $key => $val) {
667 1
            if (!$collection->contains($val)) {
668 1
                $collection->set($key, $val);
669 1
            }
670 1
        }
671
672 1
        return $collection;
673
    }
674
675
    /**
676
     * Remove all duplicate values from collection (in-place)
677
     *
678
     * @return Collection
679
     */
680 1
    public function deduplicate()
681
    {
682 1
        $this->items = array_unique($this->items);
683
684 1
        return $this;
685
    }
686
687
    /**
688
     * Get frequency of each distinct item in collection
689
     *
690
     * Returns a new collection with each distinct scalar value converted to a string as its keys and the number if
691
     * times it occurs in the collection (its frequency) as its values. Non-scalar values will simply be discarded.
692
     *
693
     * @return Collection
694
     */
695 5
    public function frequency()
696
    {
697
        return $this->fold(function(Collection $freq, $val) {
698 4
            if (is_scalar($val)) {
699 4
                $str = (string) $val;
700 4
                if (!isset($freq[$str])) {
701 4
                    $freq[$str] = 0;
702 4
                }
703 4
                $freq[$str] += 1;
704 4
            }
705 4
            return $freq;
706 5
        }, new Collection);
707
    }
708
709
    /**
710
     * Get new collection with only filtered values
711
     *
712
     * Loops through every item in the collection, applying the given callback and creating a new collection with only
713
     * those items which return true from the callback. The callback should accept the standard arguments
714
     * ($value, $key, $index). If no callback is provided, items with "truthy" values will be kept.
715
     *
716
     * @param callable $callback A callback function used to determine which items are kept (optional)
717
     *
718
     * @return Collection
719
     */
720 12
    public function filter(callable $callback = null)
721
    {
722 12
        $collection = static::factory();
723 12
        $index = 0;
724 12
        foreach ($this as $key => $value) {
725 9
            if (is_null($callback)) {
726 1
                if ($value) {
727 1
                    $collection->set($key, $value);
728 1
                }
729 1
            } else {
730 8
                if ($callback($value, $key, $index++)) {
731 8
                    $collection->set($key, $value);
732 8
                }
733
            }
734 12
        }
735
736 12
        return $collection;
737
    }
738
739
    /**
740
     * Fold collection into a single value (a.k.a. reduce)
741
     *
742
     * Apply a callback function to each item in the collection, passing the result to the next call until only a single
743
     * value remains. The arguments provided to this callback are ($folded, $val, $key, $index) where $folded is the
744
     * result of the previous call (or if the first call it is equal to the $initial param).
745
     *
746
     * @param callable $callback The callback function used to "fold" or "reduce" the collection into a single value
747
     * @param mixed $initial The (optional) initial value to pass to the callback
748
     *
749
     * @return mixed
750
     */
751 15
    public function fold(callable $callback, $initial = null)
752
    {
753 15
        $index = 0;
754 15
        $folded = $initial;
755 15
        foreach ($this as $key => $val) {
756 13
            $folded = $callback($folded, $val, $key, $index++);
757 15
        }
758
759 15
        return $folded;
760
    }
761
762
    /**
763
     * Fold collection into a single value (in opposite direction of fold)
764
     *
765
     * Folds/reduces a collection in the same way as fold(), except that it starts from the end and works backwards.
766
     *
767
     * @param callable $callback The callback function used to "fold" or "reduce" the collection into a single value
768
     * @param mixed $initial The (optional) initial value to pass to the callback
769
     *
770
     * @return mixed
771
     */
772 1
    public function foldr(callable $callback, $initial = null)
773
    {
774 1
        return $this->reverse()->fold($callback, $initial);
775
    }
776
777
    /**
778
     * Create a new collection by looping over this one
779
     *
780
     * Behaves in much the same way as fold, except that the accumulator is automatically a new collection which is
781
     * ultimately returned.
782
     *
783
     * @param callable $callback The callback used to create the new collection
784
     *
785
     * @return Collection
786
     */
787 1
    public function recollect(callable $callback)
788
    {
789 1
        return $this->fold($callback, static::factory());
790
    }
791
792
    /**
793
     * Return a merge of this collection and $items
794
     *
795
     * Returns a new collection with a merge of this collection and $items. Values from $items will overwrite values in
796
     * the current collection.
797
     *
798
     * @param array|Traversable $items The items to merge with the collection
799
     *
800
     * @return Collection
801
     */
802 3
    public function merge($items)
803
    {
804 3
        if (!is_traversable($items)) {
805 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
806
        }
807
808 2
        $collection = clone $this;
809 2
        foreach ($items as $key => $val) {
810 2
            $collection->set($key, $val);
811 2
        }
812
813 2
        return $collection;
814
    }
815
816
    /**
817
     * Create a new collection with a union of this collection and $items
818
     *
819
     * This method is similar to merge, except that existing values will not be overwritten.
820
     *
821
     * @param $items
822
     */
823 2
    public function union($items)
824
    {
825 2
        if (!is_traversable($items)) {
826 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
827
        }
828
829 1
        $collection = clone $this;
830 1
        foreach ($items as $key => $val) {
831 1
            $collection->set($key, $val, false);
832 1
        }
833
834 1
        return $collection;
835
    }
836
837
    /**
838
     * Apply a callback function to each item in the collection passively
839
     *
840
     * To stop looping through the items in the collection, return false from the callback.
841
     *
842
     * @param callable $callback The callback to use on each item in the collection
843
     *
844
     * @return self
845
     */
846 2
    public function each(callable $callback)
847
    {
848 2
        $index = 0;
849 2
        foreach ($this as $key => $val) {
850 2
            if ($callback($val, $key, $index++) === false) {
851 1
                break;
852
            }
853 2
        }
854
855 2
        return $this;
856
    }
857
858
    /**
859
     * Assert callback returns $expected value for each item in collection.
860
     *
861
     * This method will loop over each item in the collection, passing them to the callback. If the callback doesn't
862
     * return $expected value for every item in the collection, it will return false.
863
     *
864
     * @param callable $callback Assertion callback
865
     * @param bool $expected Expected value from callback
866
     *
867
     * @return bool
868
     */
869 12
    public function assert(callable $callback, $expected = true)
870
    {
871 12
        $index = 0;
872 12
        foreach ($this as $key => $val) {
873 10
            if ($callback($val, $key, $index++) !== $expected) {
874 4
                return false;
875
            }
876 10
        }
877
878 10
        return true;
879
    }
880
881
    /**
882
     * Pipe collection through a callback
883
     *
884
     * Simply passes the collection as an argument to the given callback.
885
     *
886
     * @param callable $callback The callback function (passed only one arg, the collection itself)
887
     *
888
     * @return mixed
889
     */
890 1
    public function pipe(callable $callback)
891
    {
892 1
        return $callback($this);
893
    }
894
895
    /**
896
     * Get new collection in chunks of $size
897
     *
898
     * Creates a new collection of arrays of $size length. The remainder items will be placed at the end.
899
     *
900
     * @param int $size The size of the arrays you want returned
901
     *
902
     * @return Collection
903
     */
904 2
    public function chunk($size)
905
    {
906 2
        return static::factory(array_chunk($this->items, $size, true));
907
    }
908
909
    /**
910
     * Get a new collection of $count chunks
911
     *
912
     * Returns a collection of $count number of equally-sized arrays, placing remainders at the end.
913
     *
914
     * @param int $count The number of arrays you want returned
915
     *
916
     * @return Collection
917
     */
918 1
    public function split($count = 1)
919
    {
920 1
        return $this->chunk(ceil($this->count() / $count));
921
    }
922
923
    /**
924
     * Get a slice of this collection.
925
     *
926
     * Returns a collection with a slice of this collection's items, starting at $offset and continuing until $length
927
     *
928
     * @param int $offset The offset at which you want the slice to begin
929
     * @param int|null $length The length of the slice (number of items)
930
     *
931
     * @return Collection
932
     */
933 1
    public function slice($offset, $length = null)
934
    {
935 1
        return static::factory(array_slice($this->items, $offset, $length, true));
936
    }
937
938
    /**
939
     * Zip together any number of arrays/traversables
940
     *
941
     * Merges together the values from this collection with the values of each of the provided traversables at the
942
     * corresponding index. So [1,2,3] + [4,5,6] + [7,8,9] would end up [[1,4,7], [2,5,8], [3,6,9]].
943
     *
944
     * @param array|Traversable ...$items The collections/arrays to zip
945
     *
946
     * @return Collection
947
     */
948 1
    public function zip(...$items)
949
    {
950 1
        $args = [null, $this->items];
951 1
        foreach ($items as $x) {
952 1
            $args[] = to_array($x);
953 1
        }
954 1
        return static::factory(call_user_func_array('array_map', $args));
955
    }
956
957
    /**
958
     * Get every n-th item from the collection
959
     *
960
     * @param int $n Get every $n-th item
961
     */
962 1
    public function nth($n)
963
    {
964
        return $this->filter(function($val, $key, $index) use ($n) {
965 1
            return ($index+1) % $n == 0;
966 1
        });
967
    }
968
969
    /**
970
     * Get collection with only differing items
971
     *
972
     * Returns a collection containing only the items not present in *both* this collection and $items.
973
     *
974
     * @param array|Traversable $items The items to compare with
975
     *
976
     * @return Collection
977
     */
978 1
    public function diff($items)
979
    {
980 1
        return static::factory(array_diff($this->items, to_array($items)));
981
    }
982
983
    /**
984
     * Get collection with only differing items (by key)
985
     *
986
     * Returns a collection containing only the values whose keys are not present in *both* this collection and $items.
987
     *
988
     * @param array|Traversable $items The items to compare with
989
     *
990
     * @return Collection
991
     */
992 2
    public function kdiff($items)
993
    {
994 2
        return static::factory(array_diff_key($this->items, to_array($items)));
995
    }
996
997
    /**
998
     * Get collection with only intersecting items
999
     *
1000
     * Returns a collection containing only the values present in *both* this collection and $items
1001
     *
1002
     * @param array|Traversable $items The items to compare with
1003
     *
1004
     * @return Collection
1005
     */
1006 1
    public function intersect($items)
1007
    {
1008 1
        return static::factory(array_intersect($this->items, to_array($items)));
1009
    }
1010
1011
    /**
1012
     * Get collection with only intersecting items (by key)
1013
     *
1014
     * Returns a collection containing only the values whose keys are present in *both* this collection and $items
1015
     *
1016
     * @param array|Traversable $items The items to compare with
1017
     *
1018
     * @return Collection
1019
     */
1020 1
    public function kintersect($items)
1021
    {
1022 1
        return static::factory(array_intersect_key($this->items, to_array($items)));
1023
    }
1024
1025
    /**
1026
     * Remove last item in collection and return it
1027
     *
1028
     * @return mixed
1029
     */
1030 4
    public function pop()
1031
    {
1032 4
        return array_pop($this->items);
1033
    }
1034
1035
    /**
1036
     * Remove first item in collection and return it
1037
     *
1038
     * If the collection is numerically indexed, this method will re-index it from 0 after returning the item.
1039
     *
1040
     * @return mixed
1041
     */
1042 2
    public function shift()
1043
    {
1044 2
        return array_shift($this->items);
1045
    }
1046
1047
    /**
1048
     * Add item to the end of the collection
1049
     *
1050
     * @note This method is no different than add() but I included it for consistency's sake since I have the others
1051
     *
1052
     * @param mixed $item The item to add to the collection
1053
     *
1054
     * @return self
1055
     */
1056 1
    public function push($item)
1057
    {
1058 1
        return $this->add($item);
1059
    }
1060
1061
    /**
1062
     * Add item to the beginning of the collection
1063
     *
1064
     * The collection will be re-indexed if it has numeric keys.
1065
     *
1066
     * @param mixed $item The item to add to the collection
1067
     *
1068
     * @return self
1069
     */
1070 5
    public function unshift($item)
1071
    {
1072 5
        array_unshift($this->items, $item);
1073
1074 5
        return $this;
1075
    }
1076
1077
    /**
1078
     * Get new collection padded to specified $size with $value
1079
     *
1080
     * Using $value, pad the collection to specified $size. If $size is smaller or equal to the size of the collection,
1081
     * then no padding takes place. If $size is positive, padding is added to the end, while if negative, padding will
1082
     * be added to the beginning.
1083
     *
1084
     * @param int $size The number of items collection should have
1085
     * @param mixed $value The value to pad with
1086
     *
1087
     * @return Collection
1088
     */
1089 3
    public function pad($size, $value = null)
1090
    {
1091 3
        $collection = clone $this;
1092 3
        while ($collection->count() < abs($size)) {
1093 3
            if ($size > 0) {
1094 3
                $collection->add($value);
1095 3
            } else {
1096 3
                $collection->unshift($value);
1097
            }
1098 3
        }
1099
1100 3
        return $collection;
1101
    }
1102
1103
    /**
1104
     * Partition collection into two collections using a callback
1105
     *
1106
     * Iterates over each element in the collection with a callback. Items where callback returns true are placed in one
1107
     * collection and the rest in another. Finally, the two collections are placed in an array and returned for easy use
1108
     * with the list() function. ( `list($a, $b) = $col->partition(function($val, $key, $index) {})` )
1109
     *
1110
     * @param callable $callback The comparison callback
1111
     *
1112
     * @return Collection[]
1113
     */
1114 2
    public function partition(callable $callback)
1115
    {
1116 2
        $pass = static::factory();
1117 2
        $fail = static::factory();
1118
1119 2
        $index = 0;
1120 2
        foreach ($this as $key => $val) {
1121 1
            if ($callback($val, $key, $index++)) {
1122 1
                $pass->set($key, $val);
1123 1
            } else {
1124 1
                $fail->set($key, $val);
1125
            }
1126 2
        }
1127
1128 2
        return [$pass, $fail];
1129
    }
1130
1131
    /**
1132
     * Get sum of all numeric items
1133
     *
1134
     * Returns the sum of all numeric items in the collection, silently ignoring any non-numeric values.
1135
     *
1136
     * @return float|int
1137
     */
1138 5
    public function sum()
1139
    {
1140
        return $this->fold(function($accum, $val) {
1141 4
            return is_numeric($val) ? $accum + $val : $accum;
1142 5
        }, 0);
1143
    }
1144
1145
    /**
1146
     * Get product of all numeric items
1147
     *
1148
     * Returns the product of all numeric items in the collection, silently ignoring any non-numeric values.
1149
     *
1150
     * @return float|int
1151
     */
1152 3
    public function product()
1153
    {
1154 3
        if ($this->isEmpty()) {
1155 1
            return 0;
1156
        }
1157
        return $this->fold(function($accum, $val) {
1158 2
            return is_numeric($val) ? $accum * $val : $accum;
1159 2
        }, 1);
1160
    }
1161
1162
    /**
1163
     * Get average of all numeric items
1164
     *
1165
     * Returns the average of all numeric items in the collection, silently ignoring any non-numeric values.
1166
     *
1167
     * @return float|int
1168
     */
1169 3
    public function average()
1170
    {
1171 3
        $numeric = $this->filter('Noz\is_numeric');
1172 3
        if (!$count = $numeric->count()) {
1173 1
            return 0;
1174
        }
1175 2
        return $numeric->sum() / $count;
1176
    }
1177
1178
    /**
1179
     * Get the median numeric value
1180
     *
1181
     * Returns the median of all numeric items in the collection, silently ignoring any non-numeric values.
1182
     *
1183
     * @return float|int
1184
     */
1185 3
    public function median()
1186
    {
1187 3
        $numeric = $this->filter('Noz\is_numeric')->sort();
1188 3
        if (!$count = $numeric->count()) {
1189 1
            return 0;
1190
        }
1191 2
        $pos = ($count + 1) / 2;
1192 2
        if (!is_int($pos)) {
1193 2
            return ($numeric->getValueAt(floor($pos)) + $numeric->getValueAt(ceil($pos))) / 2;
1194
        }
1195 1
        return to_numeric($numeric->getValueAt($pos));
1196
    }
1197
1198
    /**
1199
     * Get the mode numeric value
1200
     *
1201
     * Returns the mode of all numeric items in the collection, silently ignoring any non-numeric values.
1202
     *
1203
     * @return float|int
1204
     */
1205 3
    public function mode()
1206
    {
1207 3
        $mode = $this->filter('Noz\is_numeric')
1208 3
            ->frequency()
1209 3
            ->sort()
1210 3
            ->keys()
1211 3
            ->pop();
1212
1213 3
        return to_numeric($mode);
1214
    }
1215
1216
    /**
1217
     * Get maximum numeric value from collection
1218
     *
1219
     * Returns the max of all numeric items in the collection, silently ignoring any non-numeric values.
1220
     *
1221
     * @return float|int
1222
     */
1223 1
    public function max()
1224
    {
1225 1
        return to_numeric(max($this->items));
1226
    }
1227
1228
    /**
1229
     * Get minimum numeric value from collection
1230
     *
1231
     * Returns the min of all numeric items in the collection, silently ignoring any non-numeric values.
1232
     *
1233
     * @return float|int
1234
     */
1235 1
    public function min()
1236
    {
1237 1
        return to_numeric(min($this->items));
1238
    }
1239
1240
    /**
1241
     * Get column values by key
1242
     *
1243
     * This method expects the collection's data to be tabular in nature (two-dimensional and for the rows to have
1244
     * consistently named keys). If the data is not structured this way, it will do the best it can but it is not meant
1245
     * for unstructured, non-tabular data so don't expect consistent results.
1246
     *
1247
     * @param string|int $column The key of the column you want to get
1248
     *
1249
     * @return Collection
1250
     */
1251 3
    public function getColumn($column)
1252
    {
1253 3
        return static::factory(array_column($this->items, $column));
1254
    }
1255
1256
    /**
1257
     * Is collection tabular?
1258
     *
1259
     * Returns true if the data in the collection is tabular in nature, meaning it is at least two-dimensional and each
1260
     * row contains the same number of values with the same keys.
1261
     *
1262
     * @return bool
1263
     */
1264 1
    public function isTabular()
1265
    {
1266 1
        $first = $this->first();
1267 1
        return $this->assert(function($row) use ($first) {
1268 1
            if (!is_traversable(($first)) || !is_traversable($row)) {
1269 1
                return false;
1270
            }
1271 1
            return Collection::factory($row)
1272 1
                ->kdiff($first)
1273 1
                ->isEmpty();
1274 1
        });
1275
    }
1276
1277
    /** ++++                  ++++ **/
1278
    /** ++ Interface Compliance ++ **/
1279
    /** ++++                  ++++ **/
1280
1281
    /**
1282
     * JSON serialize
1283
     *
1284
     * @ignore
1285
     *
1286
     * @return array
1287
     */
1288 1
    public function jsonSerialize()
1289
    {
1290 1
        return $this->toArray();
1291
    }
1292
1293
    /** ++++                  ++++ **/
1294
    /** ++ Array Access Methods ++ **/
1295
    /** ++++                  ++++ **/
1296
1297
    /**
1298
     * Does offset exist?
1299
     *
1300
     * @ignore
1301
     *
1302
     * @param mixed $offset
1303
     *
1304
     * @return bool
1305
     */
1306 5
    public function offsetExists($offset)
1307
    {
1308 5
        return $this->has($offset);
1309
    }
1310
1311
    /**
1312
     * Get item at offset
1313
     *
1314
     * @ignore
1315
     *
1316
     * @param mixed $offset
1317
     *
1318
     * @return mixed
1319
     */
1320 6
    public function offsetGet($offset)
1321
    {
1322 6
        if (!$this->has($offset)) {
1323 1
            throw new RuntimeException("Unknown offset: {$offset}");
1324
        }
1325
1326 5
        return $this->get($offset);
1327
    }
1328
1329
    /**
1330
     * Unset item at offset
1331
     *
1332
     * @ignore
1333
     *
1334
     * @param mixed $offset
1335
     *
1336
     * @return void
1337
     */
1338 1
    public function offsetUnset($offset)
1339
    {
1340 1
        $this->delete($offset);
1341 1
    }
1342
1343
    /**
1344
     * Set item at offset
1345
     *
1346
     * @ignore
1347
     *
1348
     * @param mixed $offset
1349
     * @param mixed $value
1350
     *
1351
     * @return self
1352
     */
1353 5
    public function offsetSet($offset, $value)
1354
    {
1355 5
        if (!isset($offset)) {
1356 1
            $this->add($value);
1357 1
        }
1358
1359 5
        $this->set($offset, $value);
1360 5
    }
1361
1362
    /** ++++                  ++++ **/
1363
    /** ++   Iterator Methods   ++ **/
1364
    /** ++++                  ++++ **/
1365
1366
    /**
1367
     * @ignore
1368
     */
1369 58
    public function current()
1370
    {
1371 58
        return current($this->items);
1372
    }
1373
1374
    /**
1375
     * @ignore
1376
     */
1377 58
    public function key()
1378
    {
1379 58
        return key($this->items);
1380
    }
1381
1382
    /**
1383
     * @ignore
1384
     */
1385 55
    public function next()
1386
    {
1387 55
        return next($this->items);
1388
    }
1389
1390
    /**
1391
     * @ignore
1392
     */
1393 134
    public function rewind()
1394
    {
1395 134
        reset($this->items);
1396 134
    }
1397
1398
    /**
1399
     * @ignore
1400
     */
1401 63
    public function valid()
1402
    {
1403 63
        return $this->has(key($this->items));
1404
    }
1405
1406
    /** ++++                  ++++ **/
1407
    /** ++   Countable Method   ++ **/
1408
    /** ++++                  ++++ **/
1409
1410
    /**
1411
     * Get number of items in the collection
1412
     *
1413
     * @return int
1414
     */
1415 20
    public function count()
1416
    {
1417 20
        return count($this->items);
1418
    }
1419
}