Completed
Push — master ( 4f8323...7523f4 )
by Luke
37s
created

AbstractCollection::isAllNumeric()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 4
nop 1
dl 0
loc 13
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * Nozavroni/Collections
5
 * Just another collections library for PHP5.6+.
6
 *
7
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
8
 * @author    Luke Visinoni <[email protected]>
9
 * @license   https://github.com/nozavroni/collections/blob/master/LICENSE The MIT License (MIT)
10
 */
11
namespace Noz\Collection;
12
13
use ArrayAccess;
14
use ArrayIterator;
15
use Closure;
16
use Countable;
17
use InvalidArgumentException;
18
use Iterator;
19
use OutOfBoundsException;
20
21
use function Noz\is_traversable;
22
23
/**
24
 * Class AbstractCollection.
25
 *
26
 * This is the abstract class that all other collection classes are based on.
27
 * Although it's possible to use a completely custom Collection class by simply
28
 * implementing the "Collectable" interface, extending this class gives you a
29
 * whole slew of convenient methods for free.
30
 *
31
 * @package Noz\Collection
32
 *
33
 * @author Luke Visinoni <[email protected]>
34
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
35
 *
36
 * @todo Implement Serializable, other Interfaces
37
 */
38
abstract class AbstractCollection implements
39
    ArrayAccess,
40
    Countable,
41
    Iterator
42
{
43
    /**
44
     * @var array The collection of data this object represents
45
     */
46
    protected $data = [];
47
48
    /**
49
     * @var bool True unless we have advanced past the end of the data array
50
     */
51
    protected $isValid = true;
52
53
    /**
54
     * AbstractCollection constructor.
55
     *
56
     * @param mixed $data The data to wrap
57
     */
58
    public function __construct($data = [])
59
    {
60
        $this->setData($data);
61
    }
62
63
    /**
64
     * Invoke object.
65
     *
66
     * Magic "invoke" method. Called when object is invoked as if it were a function.
67
     *
68
     * @param mixed $val   The value (depends on other param value)
69
     * @param mixed $index The index (depends on other param value)
70
     *
71
     * @return array|AbstractCollection (Depends on parameter values)
72
     */
73
    public function __invoke($val = null, $index = null)
74
    {
75
        if (is_null($val)) {
76
            if (is_null($index)) {
77
                return $this->toArray();
78
            }
79
80
            return $this->delete($index);
81
        }
82
        if (is_null($index)) {
83
            // @todo cast $val to array?
84
                return $this->merge($val);
85
        }
86
87
        return $this->set($val, $index);
88
    }
89
90
    /**
91
     * Convert collection to string.
92
     *
93
     * @return string A string representation of this collection
94
     *
95
     * @todo Remove this method, it doesn't make sense on most collections. You can add it back to CharCollection.
96
     */
97
    public function __toString()
98
    {
99
        return $this->join();
100
    }
101
102
    // BEGIN ArrayAccess methods
103
104
    /**
105
     * Whether a offset exists.
106
     *
107
     * @param mixed $offset An offset to check for.
108
     *
109
     * @return bool true on success or false on failure.
110
     *
111
     * @see http://php.net/manual/en/arrayaccess.offsetexists.php
112
     */
113
    public function offsetExists($offset)
114
    {
115
        return $this->has($offset);
116
    }
117
118
    /**
119
     * Offset to retrieve.
120
     *
121
     * @param mixed $offset The offset to retrieve.
122
     *
123
     * @return mixed Can return all value types.
124
     *
125
     * @see http://php.net/manual/en/arrayaccess.offsetget.php
126
     */
127
    public function offsetGet($offset)
128
    {
129
        return $this->get($offset, null, true);
130
    }
131
132
    /**
133
     * Offset to set.
134
     *
135
     * @param mixed $offset The offset to assign the value to.
136
     * @param mixed $value  The value to set.
137
     *
138
     * @see http://php.net/manual/en/arrayaccess.offsetset.php
139
     */
140
    public function offsetSet($offset, $value)
141
    {
142
        $this->set($offset, $value);
143
    }
144
145
    /**
146
     * Offset to unset.
147
     *
148
     * @param mixed $offset The offset to unset.
149
     *
150
     * @see http://php.net/manual/en/arrayaccess.offsetunset.php
151
     */
152
    public function offsetUnset($offset)
153
    {
154
        $this->delete($offset);
155
    }
156
157
    // END ArrayAccess methods
158
159
    // BEGIN Countable methods
160
161
    public function count()
162
    {
163
        return count($this->data);
164
    }
165
166
    // END Countable methods
167
168
    // BEGIN Iterator methods
169
170
    /**
171
     * Return the current element.
172
     *
173
     * Returns the current element in the collection. The internal array pointer
174
     * of the data array wrapped by the collection should not be advanced by this
175
     * method. No side effects. Return current element only.
176
     *
177
     * @return mixed
178
     */
179
    public function current()
180
    {
181
        return current($this->data);
182
    }
183
184
    /**
185
     * Return the current key.
186
     *
187
     * Returns the current key in the collection. No side effects.
188
     *
189
     * @return mixed
190
     */
191
    public function key()
192
    {
193
        return key($this->data);
194
    }
195
196
    /**
197
     * Advance the internal pointer forward.
198
     *
199
     * Although this method will return the current value after advancing the
200
     * pointer, you should not expect it to. The interface does not require it
201
     * to return any value at all.
202
     *
203
     * @return mixed
204
     */
205
    public function next()
206
    {
207
        $next = next($this->data);
208
        $key  = key($this->data);
209
        if (isset($key)) {
210
            return $next;
211
        }
212
        $this->isValid = false;
213
    }
214
215
    /**
216
     * Rewind the internal pointer.
217
     *
218
     * Return the internal pointer to the first element in the collection. Again,
219
     * this method is not required to return anything by its interface, so you
220
     * should not count on a return value.
221
     *
222
     * @return mixed
223
     */
224
    public function rewind()
225
    {
226
        $this->isValid = !empty($this->data);
227
228
        return reset($this->data);
229
    }
230
231
    /**
232
     * Is internal pointer in a valid position?
233
     *
234
     * If the internal pointer is advanced beyond the end of the collection, this method will return false.
235
     *
236
     * @return bool True if internal pointer isn't past the end
237
     */
238
    public function valid()
239
    {
240
        return $this->isValid;
241
    }
242
243
    public function sort($alg = null)
244
    {
245
        if (is_null($alg)) {
246
            $alg = 'natcasesort';
247
        }
248
        $alg($this->data);
249
250
        return static::factory($this->data);
251
    }
252
253
    /**
254
     * Does this collection have a value at given index?
255
     *
256
     * @param mixed $index The index to check
257
     *
258
     * @return bool
259
     */
260
    public function has($index)
261
    {
262
        return array_key_exists($index, $this->data);
263
    }
264
265
    /**
266
     * Get value at a given index.
267
     *
268
     * Accessor for this collection of data. You can optionally provide a default
269
     * value for when the collection doesn't find a value at the given index. It can
270
     * also optionally throw an OutOfBoundsException if no value is found.
271
     *
272
     * @param mixed $index   The index of the data you want to get
273
     * @param mixed $default The default value to return if none available
274
     * @param bool  $throw   True if you want an exception to be thrown if no data found at $index
275
     *
276
     * @throws OutOfBoundsException If $throw is true and $index isn't found
277
     *
278
     * @return mixed The data found at $index or failing that, the $default
279
     *
280
     * @todo Use OffsetGet, OffsetSet, etc. internally here and on set, has, delete, etc.
281
     */
282
    public function get($index, $default = null, $throw = false)
283
    {
284
        if (isset($this->data[$index])) {
285
            return $this->data[$index];
286
        }
287
        if ($throw) {
288
            throw new OutOfBoundsException(__CLASS__ . ' could not find value at index ' . $index);
289
        }
290
291
        return $default;
292
    }
293
294
    /**
295
     * Set a value at a given index.
296
     *
297
     * Setter for this collection. Allows setting a value at a given index.
298
     *
299
     * @param mixed $index The index to set a value at
300
     * @param mixed $val   The value to set $index to
301
     *
302
     * @return $this
303
     */
304
    public function set($index, $val)
305
    {
306
        $this->data[$index] = $val;
307
308
        return $this;
309
    }
310
311
    /**
312
     * Unset a value at a given index.
313
     *
314
     * Unset (delete) value at the given index.
315
     *
316
     * @param mixed $index The index to unset
317
     * @param bool  $throw True if you want an exception to be thrown if no data found at $index
318
     *
319
     * @throws OutOfBoundsException If $throw is true and $index isn't found
320
     *
321
     * @return $this
322
     */
323
    public function delete($index, $throw = false)
324
    {
325
        if (isset($this->data[$index])) {
326
            unset($this->data[$index]);
327
        } else {
328
            if ($throw) {
329
                throw new OutOfBoundsException('No value found at given index: ' . $index);
330
            }
331
        }
332
333
        return $this;
334
    }
335
336
    /**
337
     * Does this collection have a value at specified numerical position?
338
     *
339
     * Returns true if collection contains a value (any value including null)
340
     * at specified numerical position.
341
     *
342
     * @param int $pos The position
343
     *
344
     * @return bool
345
     *
346
     * @todo I feel like it would make more sense  to have this start at position 1 rather than 0
347
     */
348
    public function hasPosition($pos)
349
    {
350
        try {
351
            $this->getKeyAtPosition($pos);
352
353
            return true;
354
        } catch (OutOfBoundsException $e) {
355
            return false;
356
        }
357
    }
358
359
    /**
360
     * Return value at specified numerical position.
361
     *
362
     * @param int $pos The numerical position
363
     *
364
     * @throws OutOfBoundsException if no pair at position
365
     *
366
     * @return mixed
367
     */
368
    public function getValueAtPosition($pos)
369
    {
370
        return $this->get($this->getKeyAtPosition($pos));
371
    }
372
373
    /**
374
     * Return key at specified numerical position.
375
     *
376
     * @param int $pos The numerical position
377
     *
378
     * @throws OutOfBoundsException if no pair at position
379
     *
380
     * @return int|string
381
     */
382
    public function getKeyAtPosition($pos)
383
    {
384
        $i = 0;
385
        foreach ($this as $key => $val) {
386
            if ($i === $pos) {
387
                return $key;
388
            }
389
            $i++;
390
        }
391
        throw new OutOfBoundsException("No element at expected position: $pos");
392
    }
393
394
    /**
395
     * @param int $pos The numerical position
396
     *
397
     * @throws OutOfBoundsException if no pair at position
398
     *
399
     * @return array
400
     */
401
    public function getPairAtPosition($pos)
402
    {
403
        $pairs = $this->pairs();
404
405
        return $pairs[$this->getKeyAtPosition($pos)];
406
    }
407
408
    /**
409
     * Get collection as array.
410
     *
411
     * @return array This collection as an array
412
     */
413
    public function toArray()
414
    {
415
        $arr = [];
416
        foreach ($this as $index => $value) {
417
            if (is_object($value) && method_exists($value, 'toArray')) {
418
                $value = $value->toArray();
419
            }
420
            $arr[$index] = $value;
421
        }
422
423
        return $arr;
424
    }
425
426
    /**
427
     * Get this collection's keys as a collection.
428
     *
429
     * @return AbstractCollection Containing this collection's keys
430
     */
431
    public function keys()
432
    {
433
        return static::factory(array_keys($this->data));
434
    }
435
436
    /**
437
     * Get this collection's values as a collection.
438
     *
439
     * This method returns this collection's values but completely re-indexed (numerically).
440
     *
441
     * @return AbstractCollection Containing this collection's values
442
     */
443
    public function values()
444
    {
445
        return static::factory(array_values($this->data));
446
    }
447
448
    /**
449
     * Merge data into collection.
450
     *
451
     * Merges input data into this collection. Input can be an array or another collection.
452
     * Returns a NEW collection object.
453
     *
454
     * @param Traversable|array $data The data to merge with this collection
455
     *
456
     * @return AbstractCollection A new collection with $data merged in
457
     */
458
    public function merge($data)
459
    {
460
        $this->assertCorrectInputDataType($data);
461
        $coll = static::factory($this->data);
462
        foreach ($data as $index => $value) {
463
            $coll->set($index, $value);
464
        }
465
466
        return $coll;
467
    }
468
469
    /**
470
     * Determine if this collection contains a value.
471
     *
472
     * Allows you to pass in a value or a callback function and optionally an index,
473
     * and tells you whether or not this collection contains that value.
474
     * If the $index param is specified, only that index will be looked under.
475
     *
476
     * @param mixed|callable $value The value to check for
477
     * @param mixed          $index The (optional) index to look under
478
     *
479
     * @return bool True if this collection contains $value
480
     *
481
     * @todo Maybe add $identical param for identical comparison (===)
482
     * @todo Allow negative offset for second param
483
     */
484
    public function contains($value, $index = null)
485
    {
486
        return (bool) $this->first(function ($val, $key) use ($value, $index) {
487
            if (is_callable($value)) {
488
                $found = $value($val, $key);
489
            } else {
490
                $found = ($value == $val);
491
            }
492
            if ($found) {
493
                if (is_null($index)) {
494
                    return true;
495
                }
496
                if (is_array($index)) {
497
                    return in_array($key, $index);
498
                }
499
500
                return $key == $index;
501
            }
502
503
            return false;
504
        });
505
    }
506
507
    /**
508
     * Get duplicate values.
509
     *
510
     * Returns a collection of arrays where the key is the duplicate value
511
     * and the value is an array of keys from the original collection.
512
     *
513
     * @return AbstractCollection A new collection with duplicate values.
514
     */
515
    public function duplicates()
516
    {
517
        $dups = [];
518
        $this->walk(function ($val, $key) use (&$dups) {
519
            $dups[$val][] = $key;
520
        });
521
522
        return static::factory($dups)->filter(function ($val) {
523
            return count($val) > 1;
524
        });
525
    }
526
527
    /**
528
     * Pop an element off the end of this collection.
529
     *
530
     * @return mixed The last item in this collectio n
531
     */
532
    public function pop()
533
    {
534
        return array_pop($this->data);
535
    }
536
537
    /**
538
     * Shift an element off the beginning of this collection.
539
     *
540
     * @return mixed The first item in this collection
541
     */
542
    public function shift()
543
    {
544
        return array_shift($this->data);
545
    }
546
547
    /**
548
     * Push a item(s) onto the end of this collection.
549
     *
550
     * Returns a new collection with $items added.
551
     *
552
     * @param array ...$items Any number of arguments will be pushed onto the
553
     *
554
     * @return AbstractCollection
555
     */
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $items a bit more specific; maybe use array[].
Loading history...
556
    public function push(...$items)
557
    {
558
        // @todo Should this work on a copy of $this->data?
559
        array_push($this->data, ...$items);
560
561
        return static::factory($this->data);
562
    }
563
564
    /**
565
     * Unshift item(s) onto the beginning of this collection.
566
     *
567
     * Returns a new collection with $items added.
568
     *
569
     * @param array ...$items Items to unshift onto collection
570
     *
571
     * @return AbstractCollection
572
     */
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $items a bit more specific; maybe use array[].
Loading history...
573
    public function unshift(...$items)
574
    {
575
        // @todo Should this work on a copy of $this->data?
576
        array_unshift($this->data, ...$items);
577
578
        return static::factory($this->data);
579
    }
580
581
    /**
582
     * Pad this collection to a certain size.
583
     *
584
     * Returns a new collection, padded to the given size, with the given value.
585
     *
586
     * @param int   $size The number of items that should be in the collection
587
     * @param mixed $with The value to pad the collection with
588
     *
589
     * @return AbstractCollection A new collection padded to specified length
590
     */
591
    public function pad($size, $with = null)
592
    {
593
        return static::factory(array_pad($this->data, $size, $with));
594
    }
595
596
    /**
597
     * Apply a callback to each item in collection.
598
     *
599
     * Applies a callback to each item in collection and returns a new collection
600
     * containing each iteration's return value.
601
     *
602
     * @param callable $callback The callback to apply
603
     *
604
     * @return AbstractCollection A new collection with callback return values
605
     */
606
    public function map(callable $callback)
607
    {
608
        return static::factory(array_map($callback, $this->data));
609
    }
610
611
    /**
612
     * Apply a callback to each item in collection.
613
     *
614
     * Applies a callback to each item in collection. The callback should return
615
     * false to filter any item from the collection.
616
     *
617
     * @param callable $callback     The callback function
618
     * @param null     $extraContext Extra context to pass as third param in callback
619
     *
620
     * @return $this
621
     *
622
     * @see php.net array_walk
623
     */
624
    public function walk(callable $callback, $extraContext = null)
625
    {
626
        array_walk($this->data, $callback, $extraContext);
627
628
        return $this;
629
    }
630
631
    /**
632
     * Iterate over each item that matches criteria in callback.
633
     *
634
     * @param Closure     $callback A callback to use
635
     * @param object|null $bindTo   The object to bind to
636
     *
637
     * @return AbstractCollection
638
     */
639
    public function each(Closure $callback, $bindTo = null)
640
    {
641
        if (is_null($bindTo)) {
642
            $bindTo = $this;
643
        }
644
        if (!is_object($bindTo)) {
645
            throw new InvalidArgumentException('Second argument must be an object.');
646
        }
647
        $cb     = $callback->bindTo($bindTo);
648
        $return = [];
649
        foreach ($this as $key => $val) {
650
            if ($cb($val, $key)) {
651
                $return[$key] = $val;
652
            }
653
        }
654
655
        return static::factory($return);
656
    }
657
658
    /**
659
     * Get each key/value as an array pair.
660
     *
661
     * Returns a collection of arrays where each item in the collection is [key,value]
662
     *
663
     * @return AbstractCollection
664
     */
665
    public function pairs()
666
    {
667
        return static::factory(array_map(
668
            function ($key, $val) {
669
                return [$key, $val];
670
            },
671
            array_keys($this->data),
672
            array_values($this->data)
673
        ));
674
    }
675
676
    /**
677
     * Reduce the collection to a single value.
678
     *
679
     * Using a callback function, this method will reduce this collection to a
680
     * single value.
681
     *
682
     * @param callable $callback The callback function used to reduce
683
     * @param null     $initial  The initial carry value
684
     *
685
     * @return mixed The single value produced by reduction algorithm
686
     */
687
    public function reduce(callable $callback, $initial = null)
688
    {
689
        return array_reduce($this->data, $callback, $initial);
690
    }
691
692
    /**
693
     * Filter the collection.
694
     *
695
     * Using a callback function, this method will filter out unwanted values, returning
696
     * a new collection containing only the values that weren't filtered.
697
     *
698
     * @param callable $callback The callback function used to filter
699
     * @param int      $flag     array_filter flag(s) (ARRAY_FILTER_USE_KEY or ARRAY_FILTER_USE_BOTH)
700
     *
701
     * @return AbstractCollection A new collection with only values that weren't filtered
702
     *
703
     * @see php.net array_filter
704
     */
705
    public function filter(callable $callback, $flag = ARRAY_FILTER_USE_BOTH)
706
    {
707
        return static::factory(array_filter($this->data, $callback, $flag));
708
    }
709
710
    /**
711
     * Return the first item that meets given criteria.
712
     *
713
     * Using a callback function, this method will return the first item in the collection
714
     * that causes the callback function to return true.
715
     *
716
     * @param callable $callback The callback function
717
     *
718
     * @return null|mixed The first item in the collection that causes callback to return true
719
     */
720
    public function first(callable $callback)
721
    {
722
        foreach ($this->data as $index => $value) {
723
            if ($callback($value, $index)) {
724
                return $value;
725
            }
726
        }
727
728
        return null;
729
    }
730
731
    /**
732
     * Return the last item that meets given criteria.
733
     *
734
     * Using a callback function, this method will return the last item in the collection
735
     * that causes the callback function to return true.
736
     *
737
     * @param callable $callback The callback function
738
     *
739
     * @return null|mixed The last item in the collection that causes callback to return true
740
     */
741
    public function last(callable $callback)
742
    {
743
        $reverse = $this->reverse(true);
744
745
        return $reverse->first($callback);
746
    }
747
748
    /**
749
     * Returns collection in reverse order.
750
     *
751
     * @param null $preserveKeys True if you want to preserve collection's keys
752
     *
753
     * @return AbstractCollection This collection in reverse order.
754
     */
755
    public function reverse($preserveKeys = null)
756
    {
757
        return static::factory(array_reverse($this->data, $preserveKeys));
758
    }
759
760
    /**
761
     * Get unique items.
762
     *
763
     * Returns a collection of all the unique items in this collection.
764
     *
765
     * @return AbstractCollection This collection with duplicate items removed
766
     */
767
    public function unique()
768
    {
769
        return static::factory(array_unique($this->data));
770
    }
771
772
    /**
773
     * Join collection together using a delimiter.
774
     *
775
     * @param string $delimiter The delimiter string/char
776
     *
777
     * @return string
778
     */
779
    public function join($delimiter = '')
780
    {
781
        return implode($delimiter, $this->data);
782
    }
783
784
    /**
785
     * Counts how many times each value occurs in a collection.
786
     *
787
     * Returns a new collection with values as keys and how many times that
788
     * value appears in the collection. Works best with scalar values but will
789
     * attempt to work on collections of objects as well.
790
     *
791
     * @return AbstractCollection
792
     *
793
     * @todo Right now, collections of arrays or objects are supported via the
794
     * __toString() or spl_object_hash()
795
     * @todo NumericCollection::counts() does the same thing...
796
     */
797
    public function frequency()
798
    {
799
        $frequency = [];
800
        foreach ($this as $key => $val) {
801
            if (!is_scalar($val)) {
802
                if (!is_object($val)) {
803
                    $val = new ArrayIterator($val);
804
                }
805
806
                if (method_exists($val, '__toString')) {
807
                    $val = (string) $val;
808
                } else {
809
                    $val = spl_object_hash($val);
810
                }
811
            }
812
            if (!isset($frequency[$val])) {
813
                $frequency[$val] = 0;
814
            }
815
            $frequency[$val]++;
816
        }
817
818
        return static::factory($frequency);
819
    }
820
821
    /**
822
     * Collection factory method.
823
     *
824
     * This method will analyze input data and determine the most appropriate Collection
825
     * class to use. It will then instantiate said Collection class with the given
826
     * data and return it.
827
     *
828
     * @param mixed $data The data to wrap
829
     *
830
     * @return AbstractCollection A collection containing $data
831
     */
832
    public static function factory($data = null)
833
    {
834
        if (static::isAllObjects($data)) {
835
            $class = ObjectCollection::class;
836
        } elseif (static::isTabular($data)) {
837
            $class = TabularCollection::class;
838
        } elseif (static::isMultiDimensional($data)) {
839
            $class = MultiCollection::class;
840
        } elseif (static::isAllNumeric($data)) {
841
            $class = NumericCollection::class;
842
        } elseif (static::isCharacterSet($data)) {
843
            $class = CharCollection::class;
844
        } else {
845
            $class = Collection::class;
846
        }
847
848
        return new $class($data);
849
    }
850
851
    /**
852
     * Is data structure all objects?
853
     *
854
     * Does the data structure passed in contain only objects?
855
     *
856
     * @param mixed $data The data structure to check
857
     *
858
     * @return bool
859
     */
860
    public static function isAllObjects($data)
861
    {
862
        if (!is_traversable($data) || empty($data)) {
863
            return false;
864
        }
865
        foreach ($data as $value) {
866
            if (!is_object($value)) {
867
                return false;
868
            }
869
        }
870
871
        return true;
872
    }
873
874
    /**
875
     * Is input data tabular?
876
     *
877
     * Returns true if input data is tabular in nature. This means that it is a
878
     * two-dimensional array with the same keys (columns) for each element (row).
879
     *
880
     * @param mixed $data The data structure to check
881
     *
882
     * @return bool True if data structure is tabular
883
     */
884
    public static function isTabular($data)
885
    {
886
        if (!is_traversable($data) || empty($data)) {
887
            return false;
888
        }
889
        foreach ($data as $row) {
890
            if (!is_traversable($row)) {
891
                return false;
892
            }
893
            $columns = array_keys($row);
894
            if (!isset($cmp_columns)) {
895
                $cmp_columns = $columns;
896
            } else {
897
                if ($cmp_columns != $columns) {
898
                    return false;
899
                }
900
            }
901
            // if row contains an array it isn't tabular
902
            if (array_reduce($row, function ($carry, $item) {
903
                return is_array($item) && $carry;
904
            }, true)) {
905
                return false;
906
            }
907
        }
908
909
        return true;
910
    }
911
912
    /**
913
     * Check data for multiple dimensions.
914
     *
915
     * This method is to determine whether a data structure is multi-dimensional.
916
     * That is to say, it is a traversable structure that contains at least one
917
     * traversable structure.
918
     *
919
     * @param mixed $data The input data
920
     *
921
     * @return bool
922
     */
923
    public static function isMultiDimensional($data)
924
    {
925
        if (!is_traversable($data) || empty($data)) {
926
            return false;
927
        }
928
        foreach ($data as $elem) {
929
            if (is_traversable($elem)) {
930
                return true;
931
            }
932
        }
933
934
        return false;
935
    }
936
937
    /**
938
     * Determine if structure contains all numeric values.
939
     *
940
     * @param mixed $data The input data
941
     *
942
     * @return bool
943
     */
944
    public static function isAllNumeric($data)
945
    {
946
        if (!is_traversable($data) || empty($data)) {
947
            return false;
948
        }
949
        foreach ($data as $val) {
950
            if (!is_numeric($val)) {
951
                return false;
952
            }
953
        }
954
955
        return true;
956
    }
957
958
    /**
959
     * Is data a string of characters?
960
     *
961
     * Just checks to see if input is a string of characters or a string
962
     * of digits.
963
     *
964
     * @param mixed $data Data to check
965
     *
966
     * @return bool
967
     */
968
    public static function isCharacterSet($data)
969
    {
970
        return
971
            is_string($data) ||
972
            is_numeric($data);
973
    }
974
975
    // END Iterator methods
976
977
    /**
978
     * Set collection data.
979
     *
980
     * Sets the collection data.
981
     *
982
     * @param array $data The data to wrap
983
     *
984
     * @return $this
985
     */
986
    protected function setData($data)
987
    {
988
        if (is_null($data)) {
989
            $data = [];
990
        }
991
        $this->assertCorrectInputDataType($data);
992
        $data = $this->prepareData($data);
993
        foreach ($data as $index => $value) {
994
            $this->set($index, $value);
995
        }
996
        reset($this->data);
997
998
        return $this;
999
    }
1000
1001
    /**
1002
     * Assert input data is of the correct structure.
1003
     *
1004
     * @param mixed $data Data to check
1005
     *
1006
     * @throws InvalidArgumentException If invalid data structure
1007
     */
1008
    protected function assertCorrectInputDataType($data)
1009
    {
1010
        // @todo this is thrown whenever a collection is instantiated with the wrong data type
1011
        // it is not the right message usually... fix it.
1012
        if (!$this->isConsistentDataStructure($data)) {
1013
            throw new InvalidArgumentException(__CLASS__ . ' expected traversable data, got: ' . gettype($data));
1014
        }
1015
    }
1016
1017
    /**
1018
     * Convert input data to an array.
1019
     *
1020
     * Convert the input data to an array that can be worked with by a collection.
1021
     *
1022
     * @param mixed $data The input data
1023
     *
1024
     * @return array
1025
     */
1026
    abstract protected function prepareData($data);
1027
1028
    /**
1029
     * Determine whether data is consistent with a given collection type.
1030
     *
1031
     * This method is used to determine whether input data is consistent with a
1032
     * given collection type. For instance, CharCollection requires a string.
1033
     * NumericCollection requires an array or traversable set of numeric data.
1034
     * TabularCollection requires a two-dimensional data structure where all the
1035
     * keys are the same in every row.
1036
     *
1037
     * @param mixed $data Data structure to check for consistency
1038
     *
1039
     * @return bool
1040
     */
1041
    abstract protected function isConsistentDataStructure($data);
1042
}
1043