Completed
Pull Request — master (#49)
by Luke
10:01 queued 06:50
created

Sequence::__invoke()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 9
nop 0
dl 0
loc 23
rs 8.5906
c 0
b 0
f 0
1
<?php
2
/**
3
 * Nozavroni/Collections
4
 * Just another collections library for PHP5.6+.
5
 * @version   {version}
6
 * @copyright Copyright (c) 2017 Luke Visinoni <[email protected]>
7
 * @author    Luke Visinoni <[email protected]>
8
 * @license   https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT)
9
 */
10
namespace Noz\Collection;
11
12
use BadMethodCallException;
13
use RuntimeException;
14
15
use Iterator;
16
use ArrayAccess;
17
use Countable;
18
use Serializable;
19
use SplFixedArray;
20
use Traversable;
21
22
use Illuminate\Support\Str;
23
24
use Noz\Contracts\Arrayable;
25
use Noz\Contracts\Immutable;
26
use Noz\Contracts\Invokable;
27
use Noz\Contracts\Structure\Sequenceable;
28
29
use Noz\Traits\IsContainer;
30
use Noz\Traits\IsImmutable;
31
use Noz\Traits\IsArrayable;
32
use Noz\Traits\IsSerializable;
33
34
use function
35
    Noz\to_array,
36
    Noz\is_traversable,
37
    Noz\normalize_offset,
38
    Noz\get_range_start_end;
39
40
/**
41
 * Sequence Collection.
42
 *
43
 * A sequence is a collection with consecutive, numeric indexes, starting from zero. It is immutable, and so any
44
 * operation that requires a change to its state will return a new sequence with whatever changes were intended.
45
 * The fact that this type of collection is indexed in this way allows some very convenient and useful functionality.
46
 * For instance, you can treat a sequence as if it were a regular array, using square brackets. Unlike a regular array
47
 * however, you may use a negative index to get the n-th from the last item. You may also use a string in the form of
48
 * "$start:$end" to retrieve a "slice" of the sequence.
49
 */
50
class Sequence implements
51
    Sequenceable,
52
    ArrayAccess,
53
    Immutable,
54
    Countable,
55
    Arrayable,
56
    Invokable,
57
    Serializable,
58
    Iterator
59
{
60
    use IsImmutable,
61
        IsContainer,
62
        IsArrayable,
63
        IsSerializable;
64
65
    /**
66
     * Delimiter used to fetch slices.
67
     */
68
    const SLICE_DELIM = ':';
69
70
    /**
71
     * Fixed-size data storage array.
72
     *
73
     * @var SplFixedArray
74
     */
75
    private $data;
76
77
    /**
78
     * Sequence constructor.
79
     *
80
     * @param array|Traversable $data The data to sequence
0 ignored issues
show
Documentation introduced by
Should the type for parameter $data not be array|Traversable|null? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
81
     */
82
    public function __construct($data = null)
83
    {
84
        if (is_null($data)) {
85
            $data = [];
86
        }
87
        $this->setData($data);
88
    }
89
90
    /**
91
     * Invoke sequence.
92
93
     * A sequence is invokable as if it were a function. This allows some pretty useful functionality such as negative
94
     * indexing, sub-sequence selection, etc. Basically, any way you invoke a sequence, you're going to get back either
95
     * a single value from the sequence or a subset of it.
96
97
     * @internal param mixed $funk Either a numerical offset (positive or negative), a range string (start:end), or a
98
     * callback to be used as a filter.
99
100
     * @return mixed
101
102
     * @todo Put all the slice logic into a helper function or several
103
     */
104
    public function __invoke()
105
    {
106
        $args = func_get_args();
107
        if ($argc = count($args)) {
0 ignored issues
show
Unused Code introduced by
$argc is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
108
            $offset = array_shift($args);
109
            $count = $this->count();
110
            if (count($args)) {
111
                // if there are more args...
112
                $length =  array_shift($args);
113
            }
114
            if (Str::contains($offset, static::SLICE_DELIM)) {
115
                list($start, $length) = get_range_start_end($offset, $count);
116
            } else {
117
                $start = normalize_offset($offset, $count);
118
            }
119
            if (isset($length)) {
120
                return new static(array_slice($this->getData(), $start, $length));
121
            } else {
122
                return $this[$start];
123
            }
124
        }
125
        return $this->toArray();
126
    }
127
128
    /**
129
     * Set data in sequence.
130
     *
131
     * Any array or traversable structure passed in will be re-indexed numerically.
132
     *
133
     * @param Traversable|array $data The sequence data
134
     */
135
    private function setData($data)
136
    {
137
        if (!is_traversable($data)) {
138
            // @todo Maybe create an ImmutableException for this?
139
            throw new BadMethodCallException(sprintf(
140
                'Forbidden method call: %s',
141
                __METHOD__
142
            ));
143
        }
144
        $data = array_values(to_array($data));
145
        $this->data = SplFixedArray::fromArray($data);
146
    }
147
148
    /**
149
     * Get data.
150
     *
151
     * Get the underlying data array.
152
     *
153
     * @return array
154
     */
155
    protected function getData()
156
    {
157
        return $this->data->toArray();
158
    }
159
160
    /**
161
     * Return the current element.
162
     *
163
     * @return mixed
164
     */
165
    public function current()
166
    {
167
        return $this->data->current();
168
    }
169
170
    /**
171
     * Move forward to next element.
172
     */
173
    public function next()
174
    {
175
        $this->data->next();
176
    }
177
178
    /**
179
     * Return the key of the current element.
180
     *
181
     * @return mixed|null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use integer.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
182
     */
183
    public function key()
184
    {
185
        return $this->data->key();
186
    }
187
188
    /**
189
     * Checks if current position is valid.
190
     *
191
     * @return boolean
192
     */
193
    public function valid()
194
    {
195
        return $this->data->valid();
196
    }
197
198
    /**
199
     * Rewind the Iterator to the first element.
200
     */
201
    public function rewind()
202
    {
203
        $this->data->rewind();
204
    }
205
206
    /**
207
     * Count elements of an object.
208
     *
209
     * @return int The custom count as an integer.
210
     */
211
    public function count()
212
    {
213
        return $this->data->count();
214
    }
215
216
    /**
217
     * Get item at collection.
218
     *
219
     * This method functions as the ArrayAccess getter. Depending on whether an int, a negative int, or a string is passed, this
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
220
     *
221
     * @param int|string $offset Offset (index) to retrieve
222
     *
223
     * @return mixed
224
     */
225
    public function offsetGet($offset)
226
    {
227
        if (Str::contains($offset, static::SLICE_DELIM)) {
228
            return $this($offset)->toArray();
229
        }
230
        if ($offset < 0) {
231
            $offset = $this->count() + $offset;
232
        }
233
        return $this->data->offsetGet($offset);
234
    }
235
236
    /**
237
     * Set offset.
238
     *
239
     * Because Sequence is immutable, this operation is not allowed. Use set() instead.
240
     *
241
     * @param int $offset  Numeric offset
242
     * @param mixed $value Value
243
     */
244
    public function offsetSet($offset, $value)
245
    {
246
        throw new RuntimeException(sprintf(
247
            'Cannot set value on %s object.',
248
            __CLASS__
249
        ));
250
    }
251
252
    /**
253
     * Set value at given offset.
254
     *
255
     * Creates a copy of the sequence, setting the specified offset to the specified value (on the copy), and returns it.
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
256
     *
257
     * @param mixed $offset The index offset to set
258
     * @param mixed $value  The value to set it to
259
     *
260
     * @return $this
261
     */
262
    public function set($offset, $value)
263
    {
264
        $arr = $this->getData();
265
        $arr[$offset] = $value;
266
        return new static($arr);
267
    }
268
269
    /**
270
     *
271
     * Because Sequence is immutable, this operation is not allowed. Use set() instead.
272
     *
273
     * @param int $offset  Numeric offset
274
     */
275
    public function offsetUnset($offset)
276
    {
277
        throw new RuntimeException(sprintf(
278
            'Cannot unset value on %s object.',
279
            __CLASS__
280
        ));
281
    }
282
283
    /**
284
     * Get new sequence without specified indices.
285
     *
286
     * Creates a copy of the sequence, unsetting the specified offset(s) (on the copy), and returns it.
287
     *
288
     * @param int|string|array The offset, range, or set of indices to remove.
289
     *
290
     * @return $this
291
     */
292
    public function except($offset)
293
    {
294
        if (!is_array($offset)) {
295
            if (is_string($offset) && Str::contains($offset, static::SLICE_DELIM)) {
296
                list($start, $length) = get_range_start_end($offset, $this->count());
297
                $indices = array_slice($this->getData(), $start, $length, true);
298
            } else {
299
                $indices = array_flip([$offset]);
300
            }
301
        } else {
302
            $indices = array_flip($offset);
303
        }
304
        return $this->diffKeys($indices);
305
    }
306
307
    /**
308
     * Is there a value at specified offset?
309
     *
310
     * Returns true of there is an item in the collection at the specified numerical offset.
311
     *
312
     * @param mixed $offset The index offset to check
313
     *
314
     * @return bool
315
     */
316
    public function offsetExists($offset)
317
    {
318
        if ($offset < 0) {
319
            $offset = $this->count() + $offset;
320
        }
321
        return $this->data->offsetExists($offset);
322
    }
323
324
    /**
325
     * Get diff by index.
326
     *
327
     * @param array|Traversable$data The array/traversable
328
     *
329
     * @return static
330
     */
331
    public function diffKeys($data)
332
    {
333
        if (!is_array($data)) {
334
            $data = to_array($data);
335
        }
336
        return new static(array_diff_key(
337
            $this->getData(),
338
            $data
339
        ));
340
    }
341
342
    /**
343
     * Get diff by value.
344
     *
345
     * @param array|Traversable$data The array/traversable
346
     *
347
     * @return static
348
     */
349
    public function diff($data)
350
    {
351
        if (!is_array($data)) {
352
            $data = to_array($data);
353
        }
354
        return new static(array_diff(
355
            $this->getData(),
356
            $data
357
        ));
358
    }
359
360
    /**
361
     * Prepend item to collection.
362
     *
363
     * Prepend an item to this collection (in place).
364
     *
365
     * @param mixed $item Item to prepend to collection
366
     *
367
     * @return Sequence
368
     */
369
     public function prepend($item)
370
     {
371
         $arr = $this->getData();
372
         array_unshift($arr, $item);
373
         return new static($arr);
374
     }
375
376
    /**
377
     * Append item to collection.
378
     *
379
     * Append an item to this collection (in place).
380
     *
381
     * @param mixed $item Item to append to collection
382
     *
383
     * @return Sequence
384
     */
385
    public function append($item)
386
    {
387
        $arr = $this->getData();
388
        array_push($arr, $item);
389
        return new static($arr);
390
    }
391
392
    /**
393
     * Fold (reduce) sequence into a single value.
394
     *
395
     * @param callable $funk    A callback function
396
     * @param mixed    $initial Initial value for accumulator
397
     *
398
     * @return mixed
399
     */
400
    public function fold(callable $funk, $initial = null)
401
    {
402
        $carry = $initial;
403
        foreach ($this->getData() as $key => $val) {
404
            $carry = $funk($carry, $val, $key);
405
        }
406
        return $carry;
407
    }
408
409
    /**
410
     * Is collection empty?
411
     *
412
     * You may optionally pass in a callback which will determine if each of the items within the collection are empty.
413
     * If all items in the collection are empty according to this callback, this method will return true.
414
     *
415
     * @param callable $funk The callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $funk not be null|callable? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
416
     *
417
     * @return bool
418
     */
419
    public function isEmpty(callable $funk = null)
420
    {
421
        if (!is_null($funk)) {
422
            return $this->fold(function ($carry, $val) use ($funk) {
423
                return $carry && $funk($val);
424
            }, true);
425
        }
426
        return empty($this->data->toArray());
427
    }
428
429
    /**
430
     * Pipe collection through callback.
431
     *
432
     * Passes entire collection to provided callback and returns the result.
433
     *
434
     * @param callable $funk The callback funkshun
435
     *
436
     * @return mixed
437
     */
438
    public function pipe(callable $funk)
439
    {
440
        return $funk($this);
441
    }
442
443
    /**
444
     * Does every item return true?
445
     *
446
     * If callback is provided, this method will return true if all items in collection cause callback to return true.
447
     * Otherwise, it will return true if all items in the collection have a truthy value.
448
     *
449
     * @param callable|null $funk The callback
450
     *
451
     * @return bool
452
     */
453
    public function every(callable $funk = null)
454
    {
455
        return $this->fold(function($carry, $val, $key) use ($funk) {
456
            if (!$carry) {
457
                return false;
458
            }
459
            if (!is_null($funk)) {
460
                return $funk($val, $key);
461
            }
462
            return (bool) $val;
463
        }, true);
464
    }
465
466
    /**
467
     * Does every item return false?
468
     *
469
     * This method is the exact opposite of "all".
470
     *
471
     * @param callable|null $funk The callback
472
     *
473
     * @return bool
474
     */
475
    public function none(callable $funk = null)
476
    {
477
        return !$this->fold(function($carry, $val, $key) use ($funk) {
478
            if ($carry) {
479
                return true;
480
            }
481
            if (!is_null($funk)) {
482
                return $funk($val, $key);
483
            }
484
            return (bool) $val;
485
        }, false);
486
    }
487
488
    /**
489
     * Get first item.
490
     *
491
     * Retrieve the first item in the collection or, if a callback is provided, return the first item that, when passed
492
     * to the callback, returns true.
493
     *
494
     * @param callable|null $funk    The callback function
495
     * @param null          $default The default value
496
     *
497
     * @return mixed
498
     */
499
    public function first(callable $funk = null, $default = null)
500
    {
501
        if (is_null($funk) && $this->count()) {
502
            return $this[0];
503
        }
504
        foreach ($this as $key => $val) {
505
            if ($funk($val, $key)) {
506
                return $val;
507
            }
508
        }
509
        return $default;
510
    }
511
512
    /**
513
     * Get last item.
514
     *
515
     * Retrieve the last item in the collection or, if a callback is provided, return the last item that, when passed
516
     * to the callback, returns true.
517
     *
518
     * @param callable|null $funk    The callback function
519
     * @param null          $default The default value
520
     *
521
     * @return mixed
522
     */
523
    public function last(callable $funk = null, $default = null)
524
    {
525
        return $this->reverse()->first($funk, $default);
526
    }
527
528
    /**
529
     * Get sequence in reverse order.
530
     *
531
     * @return Sequenceable
532
     */
533
    public function reverse()
534
    {
535
        return new static(array_reverse($this->getData()));
536
    }
537
538
    /**
539
     * Return new sequence with the first item "bumped" off.
540
     *
541
     * @return Sequenceable
542
     */
543
    public function bump()
544
    {
545
        $arr = $this->getData();
546
        array_shift($arr);
547
        return new static($arr);
548
    }
549
550
    /**
551
     * Return new sequence with the last item "dropped" off.
552
     *
553
     * @return Sequenceable
554
     */
555
    public function drop()
556
    {
557
        $arr = $this->getData();
558
        array_pop($arr);
559
        return new static($arr);
560
    }
561
}
562