Completed
Pull Request — releases/v0.2.2 (#171)
by Luke
05:23
created

AbstractCollection::shift()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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