Completed
Branch refactor/164-removeoldcollecti... (7c1123)
by Luke
05:38
created

AbstractCollection::contains()   B

Complexity

Conditions 5
Paths 1

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 1
nop 2
dl 0
loc 20
rs 8.8571
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(callable $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);
0 ignored issues
show
Bug introduced by
The method bindTo cannot be called on $callback (of type callable).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
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
}