Completed
Pull Request — master (#170)
by Luke
02:40
created

AbstractCollection::unshift()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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