Completed
Push — master ( 0d3405...8fc95d )
by Luke
07:27
created

Collection::split()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Noz\Collection;
3
4
use Countable;
5
use JsonSerializable;
6
use Iterator;
7
use ArrayAccess;
8
use RuntimeException;
9
use Traversable;
10
11
use function Noz\is_traversable,
12
             Noz\to_array;
13
14
/**
15
 * Nozavroni Collection
16
 *
17
 * Basically an array wrapper with a bunch of super useful methods for working with its items and/or create new collections from its items.
18
 *
19
 * @note None of the methods in this class have a $preserveKeys param. That is by design. I don't think it's necessary.
20
 *       Instead, keys are ALWAYS preserved and if you want to NOT preserve keys, simply call Collection::values().
21
 * @todo Scour Laravel's collection methods for ideas (for instance contains($val, $key) to check key as well as value)
22
 */
23
class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
24
{
25
    /** @var array */
26
    protected $items;
27
28
    /**
29
     * Collection constructor.
30
     *
31
     * @param array $items
32
     */
33 83
    public function __construct(array $items = [])
34
    {
35 83
        $this->items = $items;
36 83
        $this->rewind();
37 83
    }
38
39
    /**
40
     * Generate a collection from an array of items.
41
     * I created this method so that it's possible to extend a collection more easily.
42
     *
43
     * @param array $items
44
     *
45
     * @todo call to_array on $items and don't require input to be an array.
46
     *
47
     * @return Collection
48
     */
49 26
    public static function factory(array $items = [])
50
    {
51 26
        return new Collection($items);
52
    }
53
54
    /**
55
     * Get collection as an array
56
     *
57
     * @return array
58
     */
59 41
    public function toArray()
60
    {
61 41
        return $this->items;
62
    }
63
64
    /**
65
     * Determine if collection has a given key
66
     *
67
     * @param mixed $key The key to look for
68
     *
69
     * @return bool
70
     */
71 41
    public function has($key)
72
    {
73 41
        return isset($this->items[$key]) || array_key_exists($key, $this->items);
74
    }
75
76
    /**
77
     * Does collection have item at position?
78
     *
79
     * Determine if collection has an item at a particular position (indexed from one).
80
     * Position can be positive and start from the beginning or it can be negative and
81
     * start from the end.
82
     *
83
     * @param int $position
84
     *
85
     * @return bool
86
     */
87 2
    public function hasValueAt($position)
88
    {
89
        try {
90 2
            $this->getKeyAt($position);
91 2
            return true;
92 2
        } catch (RuntimeException $e) {
93 2
            return false;
94
        }
95
    }
96
97
    /**
98
     * Get key at given position
99
     *
100
     * Returns the key at the given position, starting from one. Position can be positive (start from beginning) or
101
     * negative (start from the end).
102
     *
103
     * If the position does not exist, a RuntimeException is thrown.
104
     *
105
     * @param int $position
106
     *
107
     * @return string
108
     *
109
     * @throws RuntimeException
110
     */
111 6
    public function getKeyAt($position)
112
    {
113 6
        $collection = $this;
114 6
        if ($position < 0) {
115 3
            $collection = $this->reverse();
116 3
        }
117 6
        $i = 1;
118 6
        foreach ($collection as $key => $val) {
119 6
            if (abs($position) == $i++) {
120 5
                return $key;
121
            }
122 6
        }
123 3
        throw new RuntimeException("No key at position {$position}");
124
    }
125
126
    /**
127
     * Get value at given position
128
     *
129
     * Returns the value at the given position, starting from one. Position can be positive (start from beginning) or
130
     * negative (start from the end).
131
     *
132
     * If the position does not exist, a RuntimeException is thrown.
133
     *
134
     * @param int $position
135
     *
136
     * @return mixed
137
     *
138
     * @throws RuntimeException
139
     */
140 2
    public function getValueAt($position)
141
    {
142 2
        return $this->get($this->getKeyAt($position));
143
    }
144
145
    /**
146
     * Get the key of the first item found matching $item
147
     *
148
     * @todo Perhaps this should allow a callback in place of $item?
149
     *
150
     * @param mixed $item
151
     *
152
     * @return mixed|null
153
     */
154 1
    public function keyOf($item)
155
    {
156 1
        foreach ($this as $key => $val) {
157 1
            if ($item === $val) {
158 1
                return $key;
159
            }
160 1
        }
161
162 1
        return null;
163
    }
164
165
    /**
166
     * Get the offset (index) of the first item found that matches $item
167
     *
168
     * @todo Perhaps this should allow a callback in place of $item?
169
     *
170
     * @param mixed $item
171
     *
172
     * @return int|null
173
     */
174 1
    public function indexOf($item)
175
    {
176 1
        $i = 0;
177 1
        foreach ($this as $key => $val) {
178 1
            if ($item === $val) {
179 1
                return $i;
180
            }
181 1
            $i++;
182 1
        }
183
184 1
        return null;
185
    }
186
187
    /**
188
     * Get item by key, with an optional default return value
189
     *
190
     * @param mixed $key
191
     * @param mixed $default
192
     *
193
     * @return mixed
194
     */
195 8
    public function get($key, $default = null)
196
    {
197 8
        if ($this->has($key)) {
198 8
            return $this->items[$key];
199
        }
200
201 3
        return $default;
202
    }
203
204
    /**
205
     * Add an item with no regard to key
206
     *
207
     * @param mixed $value
208
     *
209
     * @return $this
210
     */
211 5
    public function add($value)
212
    {
213 5
        $this->items[] = $value;
214
215 5
        return $this;
216
    }
217
218
    /**
219
     * Set an item at a given key
220
     *
221
     * @param mixed $key
222
     * @param mixed $value
223
     * @param bool  $overwrite If false, do not overwrite existing key
224
     *
225
     * @return $this
226
     */
227 12
    public function set($key, $value, $overwrite = true)
228
    {
229 12
        if ($overwrite || !$this->has($key)) {
230 12
            $this->items[$key] = $value;
231 12
        }
232
233 12
        return $this;
234
    }
235
236
    /**
237
     * Delete an item by key
238
     *
239
     * @param mixed $key
240
     *
241
     * @return $this
242
     */
243 3
    public function delete($key)
244
    {
245 3
        unset($this->items[$key]);
246
247 3
        return $this;
248
    }
249
250
    /**
251
     * Clear the collection of all its items.
252
     *
253
     * @return $this
254
     */
255 2
    public function clear()
256
    {
257 2
        $this->items = [];
258
259 2
        return $this;
260
    }
261
262
    /**
263
     * Determine if collection contains given value
264
     *
265
     * @param mixed|callable $val
266
     * @param mixed $key
267
     *
268
     * @return bool
269
     */
270 4
    public function contains($val, $key = null)
271
    {
272 4
        $index = 0;
273 4
        foreach ($this as $k => $v) {
274 4
            $matchkey = is_null($key) || $key === $k;
275 4
            if (is_callable($val)) {
276 1
                if ($val($v, $k, $index)) {
277 1
                    return $matchkey;
278
                }
279 1
            } else {
280 3
                if ($val === $v) {
281 3
                    return $matchkey;
282
                }
283
            }
284 4
            $index++;
285 4
        }
286 2
        return false;
287
    }
288
289
    /**
290
     * Fetch item from collection by key and remove it from collection
291
     *
292
     * @param mixed $key
293
     *
294
     * @return mixed
295
     */
296 1
    public function pull($key)
297
    {
298 1
        if ($this->has($key)) {
299 1
            $value = $this->get($key);
300 1
            $this->delete($key);
301 1
            return $value;
302
        }
303 1
    }
304
305
    /**
306
     * Join collection items using a delimiter
307
     *
308
     * @param string $delim
309
     *
310
     * @return string
311
     */
312 1
    public function join($delim = '')
313
    {
314 1
        return implode($delim, $this->items);
315
    }
316
317
    /**
318
     * Determine if collection has any items
319
     *
320
     * @return bool
321
     */
322 4
    public function isEmpty()
323
    {
324 4
        return $this->count() == 0;
325
    }
326
327
    /**
328
     * Get a collection of only this collection's values (without its keys)
329
     *
330
     * @return Collection
331
     */
332 1
    public function values()
333
    {
334 1
        return static::factory(array_values($this->items));
335
    }
336
337
    /**
338
     * Get a collection of only this collection's keys
339
     *
340
     * @return Collection
341
     */
342 1
    public function keys()
343
    {
344 1
        return static::factory(array_keys($this->items));
345
    }
346
347
    /**
348
     * Get a collection with order reversed
349
     *
350
     * @return Collection
351
     */
352 6
    public function reverse()
353
    {
354 6
        return static::factory(array_reverse($this->items));
355
    }
356
357
    /**
358
     * Get a collection with keys and values flipped
359
     *
360
     * @return Collection
361
     */
362 1
    public function flip()
363
    {
364 1
        $collection = static::factory();
365 1
        foreach ($this as $key => $val) {
366 1
            $collection->set($val, $key);
367 1
        }
368 1
        return $collection;
369
    }
370
371
    /**
372
     * Shuffle the order of this collection's values
373
     *
374
     * @return Collection
375
     */
376 1
    public function shuffle()
377
    {
378 1
        shuffle($this->items);
379 1
        return $this;
380
    }
381
382
    /**
383
     * Get a random value from the collection
384
     *
385
     * @return mixed
386
     */
387 1
    public function random()
388
    {
389 1
        return $this->getValueAt(rand(1, $this->count()));
390
    }
391
392
    /**
393
     * Sort the collection (using values)
394
     *
395
     * @param callable $alg
396
     *
397
     * @return $this
398
     */
399 2
    public function sort(callable $alg = null)
400
    {
401 2
        if (is_null($alg)) {
402
            // case-sensitive string comparison is the default sorting mechanism
403 1
            $alg = 'strcmp';
404 1
        }
405 2
        uasort($this->items, $alg);
406
407 2
        return $this;
408
    }
409
410
    /**
411
     * Sort the collection (using keys)
412
     *
413
     * @param callable $alg
414
     *
415
     * @return $this
416
     */
417 2
    public function ksort(callable $alg = null)
418
    {
419 2
        if (is_null($alg)) {
420
            // case-sensitive string comparison is the default sorting mechanism
421 1
            $alg = 'strcmp';
422 1
        }
423 2
        uksort($this->items, $alg);
424
425 2
        return $this;
426
    }
427
428
    /**
429
     * Append items to collection without regard to keys
430
     *
431
     * @param array|Traversable $items
432
     *
433
     * @return $this
434
     */
435 2
    public function append($items)
436
    {
437 2
        if (!is_traversable($items)) {
438 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
439
        }
440
441 1
        foreach ($items as $val) {
442 1
            $this->add($val);
443 1
        }
444
445 1
        return $this;
446
    }
447
448
    /**
449
     * Return first item or first item where callback returns true
450
     *
451
     * @param callable|null $callback
452
     *
453
     * @return mixed|null
454
     */
455 7
    public function first(callable $callback = null)
456
    {
457 7
        $index = 0;
458 7
        foreach ($this as $key => $val) {
459 7
            if (is_null($callback) || $callback($val, $key, $index++)) {
460 7
                return $val;
461
            }
462 6
        }
463
464
        return null;
465
    }
466
467
    /**
468
     * Return last item or last item where callback returns true
469
     *
470
     * @param callable|null $callback
471
     *
472
     * @return mixed|null
473
     */
474 3
    public function last(callable $callback = null)
475
    {
476 3
        return $this->reverse()->first($callback);
477
    }
478
479
    /**
480
     * Map collection
481
     *
482
     * Create a new collection using the results of a callback function on each item in this collection.
483
     *
484
     * @param callable $callback
485
     *
486
     * @return Collection
487
     */
488 3
    public function map(callable $callback)
489
    {
490 3
        $collection = static::factory();
491
492 3
        $index = 0;
493 3
        foreach ($this as $key => $val) {
494 3
            $collection->set($key, $callback($val, $key, $index++));
495 3
        }
496
497 3
        return $collection;
498
    }
499
500
    /**
501
     * Combine collection with another traversable/collection
502
     *
503
     * Using this collection's keys, and the incoming collection's values, a new collection is created and returned.
504
     *
505
     * @param array|Traversable $items
506
     *
507
     * @return Collection
508
     */
509 5
    public function combine($items)
510
    {
511 5
        if (!is_traversable($items)) {
512 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
513
        }
514
515 4
        $items = to_array($items);
516 4
        if (count($items) != count($this->items)) {
517 1
            throw new RuntimeException("Invalid input for " . __METHOD__ . ", number of items does not match");
518
        }
519
520 3
        return static::factory(array_combine($this->items, $items));
521
    }
522
523
    /**
524
     * Get a new collection with only distinct values
525
     *
526
     * @return Collection
527
     */
528 1
    public function distinct()
529
    {
530 1
        $collection = static::factory();
531 1
        foreach ($this as $key => $val) {
532 1
            if (!$collection->contains($val)) {
533 1
                $collection->set($key, $val);
534 1
            }
535 1
        }
536
537 1
        return $collection;
538
    }
539
540
    /**
541
     * Remove all duplicate values from collection in-place
542
     *
543
     * @return Collection
544
     */
545 1
    public function deduplicate()
546
    {
547 1
        $this->items = array_unique($this->items);
548
549 1
        return $this;
550
    }
551
552
    /**
553
     * Return a new collection with only filtered keys/values
554
     *
555
     * The callback accepts value, key, index and should return true if the item should be added to the returned
556
     * collection
557
     *
558
     * @param callable $callback
559
     *
560
     * @return Collection
561
     */
562 1
    public function filter(callable $callback)
563
    {
564 1
        $collection = static::factory();
565 1
        $index = 0;
566 1
        foreach ($this as $key => $value) {
567 1
            if ($callback($value, $key, $index++)) {
568 1
                $collection->set($key, $value);
569 1
            }
570 1
        }
571
572 1
        return $collection;
573
    }
574
575
    /**
576
     * Fold collection into a single value
577
     *
578
     * Loop through collection calling a callback function and passing the result to the next iteration, eventually
579
     * returning a single value.
580
     *
581
     * @param callable $callback
582
     * @param mixed $initial
583
     *
584
     * @return null
585
     */
586 1
    public function fold(callable $callback, $initial = null)
587
    {
588 1
        $index = 0;
589 1
        $folded = $initial;
590 1
        foreach ($this as $key => $val) {
591 1
            $folded = $callback($folded, $val, $key, $index++);
592 1
        }
593
594 1
        return $folded;
595
    }
596
597
    /**
598
     * Return a merge of this collection and $items
599
     *
600
     * @param array|Traversable $items
601
     *
602
     * @return Collection
603
     */
604 3
    public function merge($items)
605
    {
606 3
        if (!is_traversable($items)) {
607 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
608
        }
609
610 2
        $collection = clone $this;
611 2
        foreach ($items as $key => $val) {
612 2
            $collection->set($key, $val);
613 2
        }
614
615 2
        return $collection;
616
    }
617
618
    /**
619
     * Create a new collection with a union of this collection and $items
620
     *
621
     * This method is similar to merge, except that existing items will not be overwritten.
622
     *
623
     * @param $items
624
     */
625 2
    public function union($items)
626
    {
627 2
        if (!is_traversable($items)) {
628 1
            throw new RuntimeException("Invalid input type for " . __METHOD__ . ", must be array or Traversable");
629
        }
630
631 1
        $collection = clone $this;
632 1
        foreach ($items as $key => $val) {
633 1
            $collection->set($key, $val, false);
634 1
        }
635
636 1
        return $collection;
637
    }
638
639
    /**
640
     * Call callback for each item in collection, passively
641
     *
642
     * @param callable $callback
643
     *
644
     * @return $this
645
     */
646 1
    public function each(callable $callback)
647
    {
648 1
        $index = 0;
649 1
        foreach ($this as $key => $val) {
650 1
            $callback($val, $key, $index++);
651 1
        }
652
653 1
        return $this;
654
    }
655
656
    /**
657
     * Assert callback returns $expected value for each item in collection.
658
     *
659
     * @todo This can be used to easily make methods like all($callback) and none($callback).
660
     *
661
     * @param callable $callback
662
     * @param bool $expected
663
     *
664
     * @return bool
665
     */
666 1
    public function assert(callable $callback, $expected = true)
667
    {
668 1
        $index = 0;
669 1
        foreach ($this as $key => $val) {
670 1
            if ($callback($val, $key, $index++) !== $expected) {
671 1
                return false;
672
            }
673 1
        }
674
675 1
        return true;
676
    }
677
678
    /**
679
     * Pipe collection through a callback
680
     *
681
     * @param callable $callback
682
     *
683
     * @return mixed
684
     */
685 1
    public function pipe(callable $callback)
686
    {
687 1
        return $callback($this);
688
    }
689
690
    /**
691
     * Get new collection in chunks of $size
692
     *
693
     * Creates a new collection of arrays of $size length. The remainder items will be placed at the end.
694
     *
695
     * @param int $size
696
     *
697
     * @return Collection
698
     */
699 2
    public function chunk($size)
700
    {
701 2
        return static::factory(array_chunk($this->items, $size, true));
702
    }
703
704
    /**
705
     * Get a new collection of $count chunks
706
     *
707
     * @todo It might be useful to have a method that spreads remainder items more evenly so you don't end up with the
708
     *       last item containing only one or two items.
709
     *
710
     * @param int $count
711
     *
712
     * @return Collection
713
     */
714 1
    public function split($count = 1)
715
    {
716 1
        return $this->chunk(ceil($this->count() / $count));
717
    }
718
719
    /**
720
     * Get a slice of this collection.
721
     *
722
     * @param int $offset
723
     * @param int|null $length
724
     *
725
     * @return Collection
726
     */
727 1
    public function slice($offset, $length = null)
728
    {
729 1
        return static::factory(array_slice($this->items, $offset, $length, true));
730
    }
731
732
    /**
733
     * Get collection with only differing items
734
     *
735
     * @param array|Traversable $items
736
     *
737
     * @return Collection
738
     */
739 1
    public function diff($items)
740
    {
741 1
        return static::factory(array_diff($this->items, to_array($items)));
742
    }
743
744
    /**
745
     * Get collection with only differing items (by key)
746
     *
747
     * @param array|Traversable $items
748
     *
749
     * @return Collection
750
     */
751 1
    public function kdiff($items)
752
    {
753 1
        return static::factory(array_diff_key($this->items, to_array($items)));
754
    }
755
756
    /**
757
     * Get collection with only intersecting items
758
     *
759
     * @param array|Traversable $items
760
     *
761
     * @return Collection
762
     */
763 1
    public function intersect($items)
764
    {
765 1
        return static::factory(array_intersect($this->items, to_array($items)));
766
    }
767
768
    /**
769
     * Get collection with only intersecting items (by key)
770
     *
771
     * @param array|Traversable $items
772
     *
773
     * @return Collection
774
     */
775 1
    public function kintersect($items)
776
    {
777 1
        return static::factory(array_intersect_key($this->items, to_array($items)));
778
    }
779
780
    /**
781
     * Remove last item in collection and return it
782
     *
783
     * @return mixed
784
     */
785 1
    public function pop()
786
    {
787 1
        return array_pop($this->items);
788
    }
789
790
    /**
791
     * Remove first item in collection and return it (and re-index if numerically indexed)
792
     *
793
     * @return mixed
794
     */
795 2
    public function shift()
796
    {
797 2
        return array_shift($this->items);
798
    }
799
800
    /**
801
     * Add item to the end of the collection
802
     *
803
     * @note This method is no different than add() but I included it for consistency's sake since I have the others
804
     *
805
     * @param mixed $item
806
     *
807
     * @return $this
808
     */
809 1
    public function push($item)
810
    {
811 1
        return $this->add($item);
812
    }
813
814
    /**
815
     * Add item to the beginning of the collection (and re-index if a numerically indexed collection)
816
     *
817
     * @param mixed $item
818
     *
819
     * @return $this
820
     */
821 2
    public function unshift($item)
822
    {
823 2
        array_unshift($this->items, $item);
824
825 2
        return $this;
826
    }
827
828
    /** ++++                  ++++ **/
829
    /** ++ Interface Compliance ++ **/
830
    /** ++++                  ++++ **/
831
832
    /**
833
     * @return array
834
     */
835 1
    public function jsonSerialize()
836
    {
837 1
        return $this->toArray();
838
    }
839
840
    /** ++++                  ++++ **/
841
    /** ++ Array Access Methods ++ **/
842
    /** ++++                  ++++ **/
843
844
    /**
845
     * {@inheritDoc}
846
     */
847 1
    public function offsetExists($offset)
848
    {
849 1
        return $this->has($offset);
850
    }
851
852
    /**
853
     * {@inheritDoc}
854
     */
855 2
    public function offsetGet($offset)
856
    {
857 2
        if (!$this->has($offset)) {
858 1
            throw new RuntimeException("Unknown offset: {$offset}");
859
        }
860
861 1
        return $this->get($offset);
862
    }
863
864
    /**
865
     * {@inheritDoc}
866
     */
867 1
    public function offsetUnset($offset)
868
    {
869 1
        $this->delete($offset);
870 1
    }
871
872
    /**
873
     * {@inheritDoc}
874
     */
875 1
    public function offsetSet($offset, $value)
876
    {
877 1
        if (!isset($offset)) {
878 1
            $this->add($value);
879 1
        }
880
881 1
        $this->set($offset, $value);
882 1
    }
883
884
    /** ++++                  ++++ **/
885
    /** ++   Iterator Methods   ++ **/
886
    /** ++++                  ++++ **/
887
888
    /**
889
     * {@inheritDoc}
890
     */
891 30
    public function current()
892
    {
893 30
        return current($this->items);
894
    }
895
896
    /**
897
     * {@inheritDoc}
898
     */
899 30
    public function key()
900
    {
901 30
        return key($this->items);
902
    }
903
904
    /**
905
     * {@inheritDoc}
906
     */
907 29
    public function next()
908
    {
909 29
        return next($this->items);
910
    }
911
912
    /**
913
     * {@inheritDoc}
914
     *
915
     * @todo Should this return $this?
916
     */
917 83
    public function rewind()
918
    {
919 83
        reset($this->items);
920 83
    }
921
922
    /**
923
     * {@inheritDoc}
924
     */
925 30
    public function valid()
926
    {
927 30
        return $this->has(key($this->items));
928
    }
929
930
    /** ++++                  ++++ **/
931
    /** ++   Countable Method   ++ **/
932
    /** ++++                  ++++ **/
933
934
    /**
935
     * {@inheritDoc}
936
     */
937 7
    public function count()
938
    {
939 7
        return count($this->items);
940
    }
941
}