Completed
Push — releases/v0.2.1 ( ff9fbc...bb9532 )
by Luke
03:05
created

Collection::min()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * CSVelte: Slender, elegant CSV for PHP
4
 * Inspired by Python's CSV module and Frictionless Data and the W3C's CSV
5
 * standardization efforts, CSVelte was written in an effort to take all the
6
 * suck out of working with CSV.
7
 *
8
 * @version   v0.2.1
9
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
10
 * @author    Luke Visinoni <[email protected]>
11
 * @license   https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT)
12
 */
13
namespace CSVelte;
14
15
use \Iterator;
16
use \Countable;
17
use \ArrayAccess;
18
use \OutOfBoundsException;
19
use \InvalidArgumentException;
20
use \RuntimeException;
21
use CSVelte\Collection;
22
23
use function CSVelte\collect;
24
25
/**
26
 * Collection class.
27
 *
28
 * Represents a collection of data. This is a one-dimensional structure that is
29
 * represented internally with a simple array. It provides several very
30
 * convenient operations to be performed on its data.
31
 *
32
 * @package   CSVelte
33
 * @copyright (c) 2016, Luke Visinoni <[email protected]>
34
 * @author    Luke Visinoni <[email protected]>
35
 * @since     v0.2.1
36
 * @todo      Most of this class's methods will return a new Collection class
37
 *     rather than modify the existing class. There needs to be a clear distinction
38
 *     as to which ones don't and why. Also, some methods return a single value.
39
 *     These also need to be clear.
40
 * @todo      Need to make sure method naming, args, return values, concepts, etc.
41
 *     are consistent. This is a very large class with a LOT of methods. It will
42
 *     be very difficult to not let it blow up and get extremely messy. Go through
43
 *     and refactor each method. Make sure there is nothing superfluous and that
44
 *     everything makes sense and is intuitive to use. Also, because this class
45
 *     is so enourmous it is going to be a bitch to test. Good test coverage is
46
 *     going to require a LOT of tests. So put that on the list as well...
47
 * @todo      Implement whichever SPL classes/interfaces you can (that make sense).
48
 *     Probably a good idea to implement/extend some of these:
49
 *         Interfaces - RecursiveIterator, SeekableIterator, OuterIterator, IteratorAggregate
50
 *         Classes - FilterIterator, CallbackFilterIterator, CachingIterator, IteratorIterator, etc.
51
 * @replaces  \CSVelte\Utils
52
 */
53
class Collection implements Countable, ArrayAccess
54
{
55
    /**
56
     * Constants used as comparison operators in where() method
57
     */
58
59
    /** @var const Use this operator constant to test for identity (exact same) **/
60
    const WHERE_ID = '===';
61
62
    /** @var const Use this operator constant to test for non-identity **/
63
    const WHERE_NID = '!==';
64
65
    /** @var const Use this operator constant to test for equality **/
66
    const WHERE_EQ = '==';
67
68
    /** @var const Use this operator constant to test for non-equality **/
69
    const WHERE_NEQ = '!=';
70
71
    /** @var const Use this operator constant to test for less-than **/
72
    const WHERE_LT = '<';
73
74
    /** @var const Use this operator constant to test for greater-than or equal-to **/
75
    const WHERE_LTE = '<=';
76
77
    /** @var const Use this operator constant to test for greater-than **/
78
    const WHERE_GT = '>';
79
80
    /** @var const Use this operator constant to test for greater-than or equal-to **/
81
    const WHERE_GTE = '>=';
82
83
    /** @var const Use this operator constant to test for case insensitive equality **/
84
    const WHERE_LIKE = 'like';
85
86
    /** @var const Use this operator constant to test for case instensitiv inequality **/
87
    const WHERE_NLIKE = '!like';
88
89
    /** @var const Use this operator constant to test for descendants or instances of a class **/
90
    const WHERE_ISA = 'instanceof';
91
92
    /** @var const Use this operator constant to test for values that aren't descendants or instances of a class  **/
93
    const WHERE_NISA = '!instanceof';
94
95
    /** @var const Use this operator constant to test for internal PHP types **/
96
    const WHERE_TOF = 'typeof';
97
98
    /** @var const Use this operator constant to test for internal PHP type (negated) **/
99
    const WHERE_NTOF = '!typeof';
100
101
    /** @var const Use this operator constant to test against a regex pattern **/
102
    const WHERE_MATCH = 'match';
103
104
    /** @var const Use this operator constant to test against a regex pattern (negated) **/
105
    const WHERE_NMATCH = '!match';
106
107
    /**
108
     * Underlying array
109
     * @var array The array of data for this collection
110
     */
111
    protected $data = [];
112
113
    /**
114
     * Collection constructor.
115
     *
116
     * Set the data for this collection using $data
117
     *
118
     * @param array|Iterator|null $data Either an array or an object that can be accessed
119
     *     as if it were an array.
120
     */
121 127
    public function __construct($data = null)
122
    {
123 127
        $this->assertArrayOrIterator($data);
124 126
        if (!is_null($data)) {
125 125
            $this->setData($data);
126 125
        }
127 126
    }
128
129
    /**
130
     * Invokes the object as a function.
131
     *
132
     * If called with no arguments, it will return underlying data array
133
     * If called with array as first argument, array will be merged into data array
134
     * If called with second param, it will call $this->set($key, $val)
135
     * If called with null as first param and key as second param, it will call $this->offsetUnset($key)
136
     *
137
     * @param null|array $val If an array, it will be merged into the collection
138
     *     If both this arg and second arg are null, underlying data array will be returned
139
     * @param null|any $key If null and first arg is callable, this method will call map with callable
140
     *     If this value is not null but first arg is, it will call $this->offsetUnset($key)
141
     *     If this value is not null and first arg is anything other than callable, it will return $this->set($key, $val)
142
     * @see the description for various possible method signatures
143
     * @return mixed The return value depends entirely upon the arguments passed
144
     *     to it. See description for various possible arguments/return value combinations
145
     */
146 6
    public function __invoke($val = null, $key = null)
147
    {
148 6
        if (is_null($val)) {
149 3
            if (is_null($key)) {
150 3
                return $this->data;
151
            } else {
152 1
                return $this->offsetUnset($key);
153
            }
154
        } else {
155 4
            if (is_null($key)) {
156 3
                if (is_array($val)) return $this->merge($val);
157
                else {
158 2
                    if (is_callable($val)) {
159 2
                        return $this->map($val);
160
                    }
161
                }
162
            } else {
163 1
                $this->offsetSet($key, $val);
164
            }
165
        }
166 1
        return $this;
167
    }
168
169
    /**
170
     * Set internal collection data.
171
     *
172
     * Use an array or iterator to set this collection's data.
173
     *
174
     * @param array|Iterator $data The data to set for this collection
175
     * @return $this
176
     * @throws InvalidArgumentException If invalid data type
177
     */
178 125
    protected function setData($data)
179
    {
180 125
        $this->assertArrayOrIterator($data);
181 125
        foreach ($data as $key => $val) {
182 125
            $this->data[$key] = $val;
183 125
        }
184 125
        return $this;
185
    }
186
187
    /**
188
     * Get data as an array.
189
     *
190
     * @return array Collection data as an array
191
     */
192 63
    public function toArray()
193
    {
194 63
        $data = [];
195 63
        foreach($this->data as $key => $val) {
196 63
            $data[$key] = (is_object($val) && method_exists($val, 'toArray')) ? $val->toArray() : $val;
197 63
        }
198 63
        return $data;
199
    }
200
201
    /**
202
     * Get array keys
203
     *
204
     * @return \CSVelte\Collection The collection's keys (as a collection)
205
     */
206 3
    public function keys()
207
    {
208 3
        return new self(array_keys($this->data));
209
    }
210
211
    /**
212
     * Merge data (array or iterator)
213
     *
214
     * Pass an array to this method to have it merged into the collection. A new
215
     * collection will be created with the merged data and returned.
216
     *
217
     * @param array|iterator $data Data to merge into the collection
218
     * @param boolean $overwrite Whether existing values should be overwritten
219
     * @return \CSVelte\Collection A new collection with $data merged into it
220
     */
221 2
    public function merge($data = null, $overwrite = true)
222
    {
223 2
        $this->assertArrayOrIterator($data);
224 2
        $coll = new self($this->data);
225 2
        if (is_null($data)) {
226
            return $coll;
227
        }
228 2
        foreach ($data as $key => $val) {
229 2
            $coll->set($key, $val, $overwrite);
230 2
        }
231 2
        return $coll;
232
    }
233
234
    /**
235
     * Test whether this collection contains the given value, optionally at a
236
     * specific key.
237
     *
238
     * This will return true if the collection contains a value equivalent to $val.
239
     * If $val is a callable (function/method), than the callable will be called
240
     * with $val, $key as its arguments (in that order). If the callable returns
241
     * any truthy value, than this method will return true.
242
     *
243
     * @param any|callable $val Either the value to check for or a callable that
244
     *     accepts $key,$val and returns true if collection contains $val
245
     * @param any $key If not null, the only the value for this key will be checked
246
     * @return boolean True if this collection contains $val, $key
247
     */
248 49
    public function contains($val, $key = null)
249
    {
250 49
        if (is_callable($callback = $val)) {
251 48
            foreach ($this->data as $key => $val) {
252 48
                if ($callback($val, $key)) return true;
253 40
            }
254 42
        } elseif (in_array($val, $this->data)) {
255 9
            return (is_null($key) || (isset($this->data[$key]) && $this->data[$key] == $val));
256
        }
257 42
        return false;
258
    }
259
260
    /**
261
     * Tabular Where Search.
262
     *
263
     * Search for values of a certain key that meet a particular search criteria
264
     * using either one of the "Collection::WHERE_" class constants, or its string
265
     * counterpart.
266
     *
267
     * Warning: Only works for tabular collections (2-dimensional data array)
268
     *
269
     * @param string $key The key to compare to $val
270
     * @param mixed|Callable $val Either a value to test against or a callable to
271
     *     run your own custom "where comparison logic"
272
     * @param string $comp The type of comparison operation ot use (such as "=="
273
     *     or "instanceof"). Must be one of the self::WHERE_* constants' values
274
     *     listed at the top of this class.
275
     * @return \CSVelte\Collection A collection of rows that meet the criteria
276
     *     specified by $key, $val, and $comp
277
     */
278 4
    public function where($key, $val, $comp = null)
279
    {
280 4
        $this->assertIsTabular();
281 3
        $data = [];
282 3
        if ($this->has($key, true)) {
0 ignored issues
show
Documentation introduced by
$key is of type string, but the function expects a object<CSVelte\any>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
283 3
            if (is_callable($val)) {
284 1
                foreach ($this->data as $ln => $row) {
285 1
                    if ($val($row[$key], $key)) {
286 1
                        $data[$ln] = $row;
287 1
                    }
288 1
                }
289 1
            } else {
290 3
                foreach ($this->data as $ln => $row) {
291 3
                    $fieldval = $row[$key];
292 3
                    switch (strtolower($comp)) {
293 3
                        case self::WHERE_ID:
294 1
                            $comparison = $fieldval === $val;
295 1
                            break;
296 3
                        case self::WHERE_NID:
297 1
                            $comparison = $fieldval !== $val;
298 1
                            break;
299 3
                        case self::WHERE_LT:
300
                            $comparison = $fieldval < $val;
301
                            break;
302 3
                        case self::WHERE_LTE:
303 1
                            $comparison = $fieldval <= $val;
304 1
                            break;
305 3
                        case self::WHERE_GT:
306 1
                            $comparison = $fieldval > $val;
307 1
                            break;
308 3
                        case self::WHERE_GTE:
309
                            $comparison = $fieldval >= $val;
310
                            break;
311 3
                        case self::WHERE_LIKE:
312 1
                            $comparison = strtolower($fieldval) == strtolower($val);
313 1
                            break;
314 3
                        case self::WHERE_NLIKE:
315 1
                            $comparison = strtolower($fieldval) != strtolower($val);
316 1
                            break;
317 3
                        case self::WHERE_ISA:
318 1
                            $comparison = (is_object($fieldval) && $fieldval instanceof $val);
319 1
                            break;
320 3
                        case self::WHERE_NISA:
321 1
                            $comparison = (!is_object($fieldval) || !($fieldval instanceof $val));
322 1
                            break;
323 3
                        case self::WHERE_TOF:
324 1
                            $comparison = (strtolower(gettype($fieldval)) == strtolower($val));
325 1
                            break;
326 3
                        case self::WHERE_NTOF:
327 1
                            $comparison = (strtolower(gettype($fieldval)) != strtolower($val));
328 1
                            break;
329 3
                        case self::WHERE_NEQ:
330 1
                            $comparison = $fieldval != $val;
331 1
                            break;
332 2
                        case self::WHERE_MATCH:
333 1
                            $match = preg_match($val, $fieldval);
334 1
                            $comparison = $match === 1;
335 1
                            break;
336 2
                        case self::WHERE_NMATCH:
337 1
                            $match = preg_match($val, $fieldval);
338 1
                            $comparison = $match === 0;
339 1
                            break;
340 1
                        case self::WHERE_EQ:
341 1
                        default:
342 1
                            $comparison = $fieldval == $val;
343 1
                            break;
344 3
                    }
345 3
                    if ($comparison) {
346 3
                        $data[$ln] = $row;
347 3
                    }
348 3
                }
349
            }
350 3
        }
351 3
        return new self($data);
352
    }
353
354
    /**
355
     * Get the key at a given numerical position.
356
     *
357
     * This method will give you the key at the specified numerical offset,
358
     * regardless of how it's indexed (associatively, unordered numerical, etc.).
359
     * This allows you to find out what the first key is. Or the second. etc.
360
     *
361
     * @param int $pos Numerical position
362
     * @return mixed The key at numerical position
363
     * @throws \OutOfBoundsException If you request a position that doesn't exist
364
     * @todo Allow negative $pos to start counting from end
365
     */
366 66
    public function getKeyAtPosition($pos)
367
    {
368 66
        $i = 0;
369 66
        foreach ($this->data as $key => $val) {
370 66
            if ($i === $pos) return $key;
371 13
            $i++;
372 18
        }
373 17
        throw new OutOfBoundsException("Collection data does not contain a key at given position: " . $pos);
374
    }
375
376
    /**
377
     * Get the value at a given numerical position.
378
     *
379
     * This method will give you the value at the specified numerical offset,
380
     * regardless of how it's indexed (associatively, unordered numerical, etc.).
381
     * This allows you to find out what the first value is. Or the second. etc.
382
     *
383
     * @param int $pos Numerical position
384
     * @return mixed The value at numerical position
385
     * @throws \OutOfBoundsException If you request a position that doesn't exist
386
     * @todo Allow negative $pos to start counting from end
387
     */
388 65
    public function getValueAtPosition($pos)
389
    {
390 65
        return $this->data[$this->getKeyAtPosition($pos)];
391
    }
392
393
    /**
394
     * Determine if this collection has a value at the specified numerical position.
395
     *
396
     * @param int $pos Numerical position
397
     * @return boolean Whether there exists a value at specified position
398
     */
399 58
    public function hasPosition($pos)
400
    {
401
        try {
402 58
            $this->getKeyAtPosition($pos);
403 58
        } catch (OutOfBoundsException $e) {
404 9
            return false;
405
        }
406 58
        return true;
407
    }
408
409
    /**
410
     * "Pop" an item from the end of a collection.
411
     *
412
     * Removes an item from the bottom of the collection's underlying array and
413
     * returns it. This will actually remove the item from the collection.
414
     *
415
     * @param boolean $discard Whether to discard the popped item and return
416
     *     $this instead of the default behavior
417
     * @return mixed Whatever the last item in the collection is
418
     */
419 1
    public function pop($discard = false)
420
    {
421 1
        $popped = array_pop($this->data);
422 1
        return ($discard) ? $this : $popped;
423
    }
424
425
    /**
426
     * "Shift" an item from the top of a collection.
427
     *
428
     * Removes an item from the top of the collection's underlying array and
429
     * returns it. This will actually remove the item from the collection.
430
     *
431
     * @param boolean $discard Whether to discard the shifted item and return
432
     *     $this instead of the default behavior
433
     * @return mixed Whatever the first item in the collection is
434
     */
435 22
    public function shift($discard = false)
436
    {
437 22
        $shifted = array_shift($this->data);
438 22
        return ($discard) ? $this : $shifted;
439
    }
440
441
    /**
442
     * "Push" an item onto the end of the collection.
443
     *
444
     * Adds item(s) to the end of the collection's underlying array.
445
     *
446
     * @param mixed ... The item(s) to push onto the end of the collection. You may
447
     *     also add additional arguments to push multiple items onto the end
448
     * @return $this
449
     */
450 22
    public function push()
451
    {
452 22
        foreach (func_get_args() as $arg) {
453 22
            array_push($this->data, $arg);
454 22
        }
455 22
        return $this;
456
    }
457
458
    /**
459
     * "Unshift" an item onto the beginning of the collection.
460
     *
461
     * Adds item(s) to the beginning of the collection's underlying array.
462
     *
463
     * @param mixed ... The item(s) to push onto the top of the collection. You may
464
     *     also add additional arguments to add multiple items
465
     * @return $this
466
     */
467 1
    public function unshift()
468
    {
469 1
        foreach (array_reverse(func_get_args()) as $arg) {
470 1
            array_unshift($this->data, $arg);
471 1
        }
472 1
        return $this;
473
    }
474
475
    /**
476
     * "Insert" an item at a given numerical position.
477
     *
478
     * Regardless of how the collection is keyed (numerically or otherwise), this
479
     * method will insert an item at a given numerical position. If the given
480
     * position is more than there are items in the collection, the given item
481
     * will simply be added to the end. Nothing is overwritten with this method.
482
     * All elements that come after $offset will simply be shifted a space.
483
     *
484
     * Note: This method is one of the few that will modify the collection in
485
     *       place rather than returning a new one.
486
     *
487
     * @param mixed ... The item(s) to push onto the top of the collection. You may
488
     *     also add additional arguments to add multiple items
489
     * @return $this
490
     */
491 1
    public function insert($offset, $item)
492
    {
493 1
        $top = array_slice($this->data, 0, $offset);
494 1
        $bottom = array_slice($this->data, $offset);
495 1
        $this->data = array_merge($top, [$item], $bottom);
496 1
        return $this;
497
    }
498
499
    /**
500
     * Pad collection to specified length.
501
     *
502
     * Pad the collection to a specific length, filling it with a given value. A
503
     * new collection with padded values is returned.
504
     *
505
     * @param  int $size The number of values you want this collection to have
506
     * @param  any $with The value you want to pad the collection with
507
     * @return \CSVelte\Collection A new collection, padded to specified size
508
     */
509 1
    public function pad($size, $with = null)
510
    {
511 1
        return new self(array_pad($this->data, (int) $size, $with));
512
    }
513
514
    /**
515
     * Check if this collection has a value at the given key.
516
     *
517
     * If this is a tabular data collection, this will check if the table has the
518
     * given key by default. You can change this behavior by passing false as the
519
     * second argument (this will change the behavior to check for a given key
520
     * at the row-level so it will likely only ever be numerical).
521
     *
522
     * @param any $key The key you want to check
523
     * @return boolean Whether there's a value at $key
524
     */
525 27
    public function has($key, $column = true)
526
    {
527
        // we only need to check one row for the existance of $key because the
528
        // isTabular() method ensures every row has the same keys
529 27
        if ($column && $this->isTabular() && $first = reset($this->data)) {
530 3
            return array_key_exists($key, $first);
531
        }
532
        // if we don't have tabular data or we don't want to check for a column...
533 24
        return array_key_exists($key, $this->data);
534
    }
535
536
    /**
537
     * Get the value at the given key.
538
     *
539
     * If there is a value at the given key, it will be returned. If there isn't,
540
     * a default may be specified. If you would like for this method to throw an
541
     * exception when there is no value at $key, pass true as the third argument
542
     *
543
     * @param  any  $key      The key you want to test for
544
     * @param  any  $default  The default to return if there is no value at $key
545
     * @param  boolean $throwExc Whether to throw an exception on failure to find
546
     *     a value at the given key.
547
     * @return mixed            Either the value at $key or the specified default
548
     *     value
549
     * @throws \OutOfBoundsException If value can't be found at $key and $throwExc
550
     *     is set to true
551
     */
552 33
    public function get($key, $default = null, $throwExc = false)
553
    {
554 33
        if (array_key_exists($key, $this->data)) {
555 31
            return $this->data[$key];
556
        } else {
557 7
            if ($throwExc) {
558 6
                throw new OutOfBoundsException("Collection data does not contain value for given key: " . $key);
559
            }
560
        }
561 2
        return $default;
562
    }
563
564
    /**
565
     * Set value for the given key.
566
     *
567
     * Given $key, this will set $this->data[$key] to the value of $val. If that
568
     * index already has a value, it will be overwritten unless $overwrite is set
569
     * to false. In that case nothing happens.
570
     *
571
     * @param string $key The key you want to set a value for
572
     * @param mixed $value The value you want to set key to
573
     * @param boolean $overwrite Whether to overwrite existing value
574
     * @return $this
575
     */
576 26
    public function set($key, $value = null, $overwrite = true)
577
    {
578 26
        if (!array_key_exists($key, $this->data) || $overwrite) {
579 26
            $this->data[$key] = $value;
580 26
        }
581 26
        return $this;
582
    }
583
584
    /**
585
     * Unset value at the given offset.
586
     *
587
     * This method is used when the end-user uses a colleciton as an array and
588
     * calls unset($collection[5]).
589
     *
590
     * @param mixed $offset The offset at which to unset
591
     * @return $this
592
     * @todo create an alias for this... maybe delete() or remove()
593
     */
594 2
    public function offsetUnset($offset)
595
    {
596 2
        if ($this->has($offset)) {
597 2
            unset($this->data[$offset]);
598 2
        }
599 2
        return $this;
600
    }
601
602
    /**
603
     * Alias of self::has
604
     *
605
     * @param int|mixed The offset to test for
606
     * @return boolean Whether a value exists at $offset
607
     */
608
    public function offsetExists($offset)
609
    {
610
        return $this->has($offset);
611
    }
612
613
    /**
614
     * Alias of self::set
615
     *
616
     * @param int|mixed The offset to set
617
     * @param any The value to set it to
618
     * @return boolean
619
     */
620 1
    public function offsetSet($offset, $value)
621
    {
622 1
        $this->set($offset, $value);
623 1
        return $this;
624
    }
625
626
    /**
627
     * Alias of self::get
628
     *
629
     * @param int|mixed The offset to get
630
     * @return mixed The value at $offset
631
     */
632
    public function offsetGet($offset)
633
    {
634
        return $this->get($offset);
635
    }
636
637
    /**
638
     * Increment an item.
639
     *
640
     * Increment the item specified by $key by one value. Intended for integers
641
     * but also works (using this term loosely) for letters. Any other data type
642
     * it may modify is unintended behavior at best.
643
     *
644
     * This method modifies its internal data array rather than returning a new
645
     * collection.
646
     *
647
     * @param  mixed $key The key of the item you want to increment.
648
     * @return $this
649
     */
650 3
    public function increment($key)
651
    {
652 3
        $val = $this->get($key, null, true);
653 3
        $this->set($key, ++$val);
654 3
        return $this;
655
    }
656
657
    /**
658
     * Decrement an item.
659
     *
660
     * Frcrement the item specified by $key by one value. Intended for integers.
661
     * Does not work for letters and if it does anything to anything else, it's
662
     * unintended at best.
663
     *
664
     * This method modifies its internal data array rather than returning a new
665
     * collection.
666
     *
667
     * @param  mixed $key The key of the item you want to decrement.
668
     * @return $this
669
     */
670 1
    public function decrement($key)
671
    {
672 1
        $val = $this->get($key, null, true);
673 1
        $this->set($key, --$val);
674 1
        return $this;
675
    }
676
677
    /**
678
     * Count the items in this collection.
679
     *
680
     * Returns either the number of items in the collection or, if this is a
681
     * collection of tabular data, and you pass true as the first argument, you
682
     * will get back a collection containing the count of each row (which will
683
     * always be the same so maybe I should still just return an integer).
684
     *
685
     * @param boolean $multi Whether to count just the items in the collection or
686
     *     to count the items in each tabular data row.
687
     * @return int|\CSVelte\Collection Either an integer count or a collection of counts
688
     */
689 42
    public function count($multi = false)
690
    {
691 42
        if ($multi) {
692
            // if every value is an array...
693 1
            if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
694 1
                return $condRet;
1 ignored issue
show
Bug Best Practice introduced by
The return type of return $condRet; (CSVelte\Collection) is incompatible with the return type declared by the interface Countable::count of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
695
            }
696
        }
697
        // just count main array
698 42
        return count($this->data);
699
    }
700
701
    /**
702
     * Collection map.
703
     *
704
     * Apply a callback to each element in the collection and return the
705
     * resulting collection. The resulting collection will contain the return
706
     * values of each call to $callback.
707
     *
708
     * @param Callable $callback A callback to apply to each item in the collection
709
     * @return \CSVelte\Collection A collection of callback return values
710
     */
711 34
    public function map(Callable $callback)
712
    {
713 34
        return new self(array_map($callback, $this->data));
714
    }
715
716
    /**
717
     * Walk the collection.
718
     *
719
     * Walk through each item in the collection, calling a function for each
720
     * item in the collection. This is one of the few methods that doesn't return
721
     * a new collection. All changes will be to the existing collection object.
722
     *
723
     * Note: return false from the collback to stop walking.
724
     *
725
     * @param Callable $callback A callback function to call for each item in the collection
726
     * @param any $userdata Any extra data you'd like passed to your callback
727
     * @return $this
728
     */
729 34
    public function walk(Callable $callback, $userdata = null)
730
    {
731 34
        array_walk($this->data, $callback, $userdata);
732 34
        return $this;
733
    }
734
735
    /**
736
     * Call a user function for each item in the collection. If function returns
737
     * false, loop is terminated.
738
     *
739
     * @return $this
740
     * @todo I'm not entirely sure what this method should do... return new
741
     *     collection? modify this one?
742
     * @todo This method appears to be a duplicate of walk(). Is it even necessary?
743
     */
744 2
    public function each(Callable $callback)
745
    {
746 2
        foreach ($this->data as $key => $val) {
747 2
            if (!$ret = $callback($val, $key)) {
748 2
                if ($ret === false) break;
749 2
            }
750 2
        }
751 2
        return $this;
752
    }
753
754
    /**
755
     * Reduce collection to single value.
756
     *
757
     * Reduces the collection to a single value by calling a callback for each
758
     * item in the collection, carrying along an accumulative value as it does so.
759
     * The final value is then returned.
760
     *
761
     * @param Callable $callback The function to reduce the collection
762
     * @param any $initial The initial value to set the accumulative value to
763
     * @return mixed Whatever the final value from the callback is
764
     */
765
    public function reduce(Callable $callback, $initial = null)
766
    {
767
        return array_reduce($this->data, $callback, $initial);
768
    }
769
770
    /**
771
     * Filter out unwanted items using a callback function.
772
     *
773
     * @param Callable $callback
774
     * @return CSVelte\Collection A new collection with filtered items removed
775
     */
776 23
    public function filter(Callable $callback)
777
    {
778 23
        $keys = [];
779 23
        foreach ($this->data as $key => $val) {
780 23
            if (false === $callback($val, $key)) $keys[$key] = true;
781 23
        }
782 23
        return new self(array_diff_key($this->data, $keys));
783
    }
784
785
    /**
786
     * Get first match.
787
     *
788
     * Get first value that meets criteria specified with $callback function.
789
     *
790
     * @param Callable $callback A callback with arguments ($val, $key). If it
791
     *     returns true, that $val will be returned.
792
     * @return mixed The first $val that meets criteria specified with $callback
793
     */
794 1
    public function first(Callable $callback)
795
    {
796 1
        foreach ($this->data as $key => $val) {
797 1
            if ($callback($val, $key)) return $val;
798 1
        }
799
        return null;
800
    }
801
802
    /**
803
     * Get last match.
804
     *
805
     * Get last value that meets criteria specified with $callback function.
806
     *
807
     * @param Callable $callback A callback with arguments ($val, $key). If it
808
     *     returns true, that $val will be returned.
809
     * @return mixed The last $val that meets criteria specified with $callback
810
     */
811 1
    public function last(Callable $callback)
812
    {
813 1
        $elem = null;
814 1
        foreach ($this->data as $key => $val) {
815 1
            if ($callback($val, $key)) $elem = $val;
816 1
        }
817 1
        return $elem;
818
    }
819
820
    /**
821
     * Collection value frequency.
822
     *
823
     * Returns an array where the key is a value in the collection and the value
824
     * is the number of times that value appears in the collection.
825
     *
826
     * @return CSVelte\Collection A collection of value frequencies (see description)
827
     */
828 23
    public function frequency()
829
    {
830 23
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
831 22
            return $condRet;
832
        }
833 23
        $freq = [];
834 23
        foreach ($this->data as $val) {
835 23
            $key = is_numeric($val) ? $val : (string) $val;
836 23
            if (!isset($freq[$key])) {
837 23
                $freq[$key] = 0;
838 23
            }
839 23
            $freq[$key]++;
840 23
        }
841 23
        return new self($freq);
842
    }
843
844
    /**
845
     * Unique collection.
846
     *
847
     * Returns a collection with duplicate values removed. If two-dimensional,
848
     * then each array within the collection will have its duplicates removed.
849
     *
850
     * @return CSVelte\Collection A new collection with duplicate values removed.
851
     */
852 23
    public function unique()
853
    {
854 23
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
855 6
            return $condRet;
856
        }
857 20
        return new self(array_unique($this->data));
858
    }
859
860
    /**
861
     * Get duplicate values.
862
     *
863
     * Returns a collection of arrays where the key is the duplicate value
864
     * and the value is an array of keys from the original collection.
865
     *
866
     * @return CSVelte\Collection A new collection with duplicate values.
867
     */
868 7
    public function duplicates()
869
    {
870 7
        $dups = [];
871
        $this->walk(function($val, $key) use (&$dups) {
872 7
            $dups[$val][] = $key;
873 7
        });
874
        return (new self($dups))->filter(function($val) {
875 7
            return (count($val) > 1);
876 7
        });
877
    }
878
879
    /**
880
     * Reverse keys/values.
881
     *
882
     * Get a new collection where the keys and values have been swapped.
883
     *
884
     * @return CSVelte\Collection A new collection where keys/values have been swapped
885
     */
886 1
    public function flip()
887
    {
888 1
        return new self(array_flip($this->data));
889
    }
890
891
    /**
892
     * Return an array of key/value pairs.
893
     *
894
     * Return array can either be in [key,value] or [key => value] format. The
895
     * first is the default.
896
     *
897
     * @param boolean Whether you want pairs in [k => v] rather than [k, v] format
898
     * @return CSVelte\Collection A collection of key/value pairs
899
     */
900 1
    public function pairs($alt = false)
901
    {
902 1
        return new self(array_map(
903
            function ($key, $val) use ($alt) {
904 1
                if ($alt) {
905 1
                    return [$key => $val];
906
                } else {
907 1
                    return [$key, $val];
908
                }
909 1
            },
910 1
            array_keys($this->data),
911 1
            array_values($this->data)
912 1
        ));
913
    }
914
915
    /**
916
     * Get average of data items.
917
     *
918
     * @return mixed The average of all items in collection
919
     */
920 2
    public function sum()
921
    {
922 2
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
923 1
            return $condRet;
924
        }
925 2
        $this->assertNumericValues();
926 2
        return array_sum($this->data);
927
    }
928
929
    /**
930
     * Get average of data items.
931
     *
932
     * If two-dimensional it will return a collection of averages.
933
     *
934
     * @return mixed|CSVelte\Collection The average of all items in collection
935
     */
936 9
    public function average()
937
    {
938 9
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
939 7
            return $condRet;
940
        }
941 9
        $this->assertNumericValues();
942 9
        $total = array_sum($this->data);
943 9
        $count = count($this->data);
944 9
        return $total / $count;
945
    }
946
947
    /**
948
     * Get largest item in the collection
949
     *
950
     * @return mixed The largest item in the collection
951
     */
952 9
    public function max()
953
    {
954 9
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
955 1
            return $condRet;
956
        }
957 9
        $this->assertNumericValues();
958 9
        return max($this->data);
959
    }
960
961
    /**
962
     * Get smallest item in the collection
963
     *
964
     * @return mixed The smallest item in the collection
965
     */
966 5
    public function min()
967
    {
968 5
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
969 1
            return $condRet;
970
        }
971 5
        $this->assertNumericValues();
972 5
        return min($this->data);
973
    }
974
975
    /**
976
     * Get mode of data items.
977
     *
978
     * @return mixed The mode of all items in collection
979
     */
980 9
    public function mode()
981
    {
982 9
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
983 7
            return $condRet;
984
        }
985
        $strvals = $this->map(function($val){
986 9
            return (string) $val;
987 9
        });
988 9
        $this->assertNumericValues();
989 8
        $counts = array_count_values($strvals->toArray());
990 8
        arsort($counts);
991 8
        $mode = key($counts);
992 8
        return (strpos($mode, '.')) ? floatval($mode) : intval($mode);
993
    }
994
995
    /**
996
     * Get median of data items.
997
     *
998
     * @return mixed The median of all items in collection
999
     */
1000 2
    public function median()
1001
    {
1002 2
        if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) {
1003 1
            return $condRet;
1004
        }
1005 2
        $this->assertNumericValues();
1006 2
        $count = count($this->data);
1007 2
        natcasesort($this->data);
1008 2
        $middle = ($count / 2);
1009 2
        $values = array_values($this->data);
1010 2
        if ($count % 2 == 0) {
1011
            // even number, use middle
1012 2
            $low = $values[$middle - 1];
1013 2
            $high = $values[$middle];
1014 2
            return ($low + $high) / 2;
1015
        }
1016
        // odd number return median
1017 2
        return $values[$middle];
1018
    }
1019
1020
    /**
1021
     * Join items together into a string
1022
     *
1023
     * @param string $glue The string to join items together with
1024
     * @return string A string with all items in the collection strung together
1025
     * @todo Make this work with 2D collection
1026
     */
1027 18
    public function join($glue)
1028
    {
1029 18
        return implode($glue, $this->data);
1030
    }
1031
1032
    /**
1033
     * Is the collection empty?
1034
     *
1035
     * @return boolean Whether the collection is empty
1036
     */
1037 1
    public function isEmpty()
1038
    {
1039 1
        return empty($this->data);
1040
    }
1041
1042
    /**
1043
     * Immediately invoke a callback.
1044
     *
1045
     * @param Callable $callback A callback to invoke with ($this)
1046
     * @return mixed Whatever the callback returns
1047
     */
1048 1
    public function value(Callable $callback)
1049
    {
1050 1
        return $callback($this);
1051
    }
1052
1053
    /**
1054
     * Sort the collection.
1055
     *
1056
     * This method can sort your collection in any which way you please. By
1057
     * default it uses a case-insensitive natural order algorithm, but you can
1058
     * pass it any sorting algorithm you like.
1059
     *
1060
     * @param Callable $callback The sorting function you want to use
1061
     * @param boolean $preserve_keys Whether you want to preserve keys
1062
     * @return CSVelte\Collection A new collection sorted by $callback
1063
     */
1064 23
    public function sort(Callable $callback = null, $preserve_keys = true)
1065
    {
1066 23
        if (is_null($callback)) $callback = 'strcasecmp';
1067 23
        if (!is_callable($callback)) {
1068
            throw new InvalidArgumentException(sprintf(
1069
                'Invalid argument supplied for %s. Expected %s, got: "%s".',
1070
                __METHOD__,
1071
                'Callable',
1072
                gettype($callback)
1073
            ));
1074
        }
1075 23
        $data = $this->data;
1076 23
        if ($preserve_keys) {
1077 23
            uasort($data, $callback);
1078 23
        } else {
1079 2
            usort($data, $callback);
1080
        }
1081 23
        return new self($data);
1082
    }
1083
1084
    /**
1085
     * Order tabular data.
1086
     *
1087
     * Order a tabular dataset by a given key/comparison algorithm
1088
     *
1089
     * @param string $key The key you want to order by
1090
     * @param Callable $cmp The sorting comparison algorithm to use
1091
     * @param boolean $preserve_keys Whether keys should be preserved
1092
     * @return CSVelte\Collection A new collection sorted by $cmp and $key
1093
     */
1094 1
    public function orderBy($key, Callable $cmp = null, $preserve_keys = true)
1095
    {
1096 1
        $this->assertIsTabular();
1097
        return $this->sort(function($a, $b) use ($key, $cmp) {
1098 1
            if (!isset($a[$key]) || !isset($b[$key])) {
1099
                throw new RuntimeException('Cannot order collection by non-existant key: ' . $key);
1100
            }
1101 1
            if (is_null($cmp)) {
1102 1
                return strcasecmp($a[$key], $b[$key]);
1103
            } else {
1104 1
                return $cmp($a[$key], $b[$key]);
1105
            }
1106 1
        }, $preserve_keys);
1107
    }
1108
1109
    /**
1110
     * Reverse collection order.
1111
     *
1112
     * Reverse the order of items in a collection. Sometimes it's easier than
1113
     * trying to write a particular sorting algurithm that sorts forwards and back.
1114
     *
1115
     * @param boolean $preserve_keys Whether keys should be preserved
1116
     * @return CSVelte\Collection A new collection in reverse order
1117
     */
1118 22
    public function reverse($preserve_keys = true)
1119
    {
1120 22
        return new self(array_reverse($this->data, $preserve_keys));
1121
    }
1122
1123 39
    protected function if2DMapInternalMethod($method)
1124
    {
1125 39
        if ($this->is2D()) {
1126 29
            $method = explode('::', $method, 2);
1127 29
            if (count($method) == 2) {
1128 29
                $method = $method[1];
1129
                return $this->map(function($val) use ($method) {
1130 29
                    return (new self($val))->$method();
1131 29
                });
1132
            }
1133
        }
1134 38
        return false;
1135
    }
1136
1137
    /**
1138
     * Is this collection two-dimensional
1139
     *
1140
     * If all items of the collection are arrays this will return true.
1141
     *
1142
     * @return boolean whether this is two-dimensional
1143
     */
1144 47
    public function is2D()
1145
    {
1146
        return !$this->contains(function($val){
1147 47
            return !is_array($val);
1148 47
        });
1149
    }
1150
1151
    /**
1152
     * Is this a tabular collection?
1153
     *
1154
     * If this is a two-dimensional collection with the same keys in every array,
1155
     * this method will return true.
1156
     *
1157
     * @return boolean Whether this is a tabular collection
1158
     */
1159 36
    public function isTabular()
1160
    {
1161 36
        if ($this->is2D()) {
1162
            // look through each item in the collection and if an array, grab its keys
1163
            // and throw them in an array to be analyzed later...
1164 32
            $test = [];
1165
            $this->walk(function($val, $key) use (&$test) {
1166 11
                if (is_array($val)) {
1167 11
                    $test[$key] = array_keys($val);
1168 11
                    return true;
1169
                }
1170
                return false;
1171 32
            });
1172
1173
            // if the list of array keys is shorter than the total amount of items in
1174
            // the collection, than this is not tabular data
1175 32
            if (count($test) != count($this)) return false;
1176
1177
            // loop through the array of each item's array keys that we just created
1178
            // and compare it to the FIRST item. If any array contains different keys
1179
            // than this is not tabular data.
1180 32
            $first = array_shift($test);
1181 32
            foreach ($test as $key => $keys) {
1182 11
                $diff = array_diff($first, $keys);
1183 11
                if (!empty($diff)) return false;
1184 28
            }
1185 28
            return true;
1186
        }
1187 25
        return false;
1188
    }
1189
1190
    /**
1191
     * Assert this collection is two-dimensional.
1192
     *
1193
     * Although a collection must be two-dimensional to be tabular, the opposite
1194
     * is not necessarily true. This will throw an exception if this collection
1195
     * contains anything but arrays.
1196
     *
1197
     * @throws
1198
     */
1199
    protected function assertIs2D()
1200
    {
1201
        if (!$this->is2D()) {
1202
            throw new RuntimeException('Invalid data type, requires two-dimensional array.');
1203
        }
1204
    }
1205
1206 5
    protected function assertIsTabular()
1207
    {
1208 5
        if (!$this->isTabular()) {
1209 1
            throw new RuntimeException('Invalid data type, requires tabular data (two-dimensional array where each sub-array has the same keys).');
1210
        }
1211 4
    }
1212
1213
    protected function assertNumericValues()
1214
    {
1215 19
        if ($this->contains(function($val){
1216 19
            return !is_numeric($val);
1217 19
        })) {
1218
            // can't average non-numeric data
1219 1
            throw new InvalidArgumentException(sprintf(
1220 1
                "%s expects collection of integers or collection of arrays of integers",
1221
                __METHOD__
1222 1
            ));
1223
        }
1224 18
    }
1225
1226 127
    protected function assertArrayOrIterator($data)
1227
    {
1228 127
        if (is_null($data) || is_array($data) || $data instanceof Iterator) {
1229 126
            return;
1230
        }
1231 1
        throw new InvalidArgumentException("Invalid type for collection data: " . gettype($data));
1232
    }
1233
}
1234