Completed
Pull Request — master (#76)
by Luke
02:26
created

Sequence   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 516
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 0
loc 516
rs 6.5957
c 0
b 0
f 0
wmc 56
lcom 1
cbo 6

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
B __invoke() 0 23 5
A setData() 0 12 2
A getData() 0 4 1
A current() 0 4 1
A next() 0 4 1
A key() 0 4 1
A valid() 0 4 1
A rewind() 0 4 1
A count() 0 4 1
A offsetGet() 0 10 3
A offsetSet() 0 7 1
A set() 0 6 1
A offsetUnset() 0 7 1
A except() 0 14 4
A offsetExists() 0 7 2
A diffKeys() 0 10 2
A diff() 0 10 2
A prepend() 0 6 1
A append() 0 6 1
A fold() 0 8 2
A isEmpty() 0 9 3
A pipe() 0 4 1
A every() 0 12 3
A none() 0 12 3
B first() 0 12 5
A last() 0 4 1
A reverse() 0 4 1
A bump() 0 6 1
A drop() 0 6 1
A unserialize() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Sequence often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Sequence, and based on these observations, apply Extract Interface, too.

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\Immutable;
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
    Iterator
58
{
59
    use IsImmutable,
60
        IsContainer,
61
        IsArrayable,
62
        IsSerializable;
63
64
    /**
65
     * Delimiter used to fetch slices.
66
     */
67
    const SLICE_DELIM = ':';
68
69
    /**
70
     * Fixed-size data storage array.
71
     *
72
     * @var SplFixedArray
73
     */
74
    private $data;
75
76
    /**
77
     * Sequence constructor.
78
     *
79
     * @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...
80
     */
81
    public function __construct($data = null)
82
    {
83
        if (is_null($data)) {
84
            $data = [];
85
        }
86
        $this->setData($data);
87
    }
88
89
    /**
90
     * Invoke sequence.
91
92
     * A sequence is invokable as if it were a function. This allows some pretty useful functionality such as negative
93
     * indexing, sub-sequence selection, etc. Basically, any way you invoke a sequence, you're going to get back either
94
     * a single value from the sequence or a subset of it.
95
96
     * @internal param mixed $funk Either a numerical offset (positive or negative), a range string (start:end), or a
97
     * callback to be used as a filter.
98
99
     * @return mixed
100
101
     * @todo Put all the slice logic into a helper function or several
102
     */
103
    public function __invoke()
104
    {
105
        $args = func_get_args();
106
        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...
107
            $offset = array_shift($args);
108
            $count = $this->count();
109
            if (count($args)) {
110
                // if there are more args...
111
                $length =  array_shift($args);
112
            }
113
            if (Str::contains($offset, static::SLICE_DELIM)) {
114
                list($start, $length) = get_range_start_end($offset, $count);
115
            } else {
116
                $start = normalize_offset($offset, $count);
117
            }
118
            if (isset($length)) {
119
                return new static(array_slice($this->getData(), $start, $length));
120
            } else {
121
                return $this[$start];
122
            }
123
        }
124
        return $this->toArray();
125
    }
126
127
    /**
128
     * Set data in sequence.
129
     *
130
     * Any array or traversable structure passed in will be re-indexed numerically.
131
     *
132
     * @param Traversable|array $data The sequence data
133
     */
134
    private function setData($data)
135
    {
136
        if (!is_traversable($data)) {
137
            // @todo Maybe create an ImmutableException for this?
138
            throw new BadMethodCallException(sprintf(
139
                'Forbidden method call: %s',
140
                __METHOD__
141
            ));
142
        }
143
        $data = array_values(to_array($data));
144
        $this->data = SplFixedArray::fromArray($data);
145
    }
146
147
    /**
148
     * Get data.
149
     *
150
     * Get the underlying data array.
151
     *
152
     * @return array
153
     */
154
    protected function getData()
155
    {
156
        return $this->data->toArray();
157
    }
158
159
    /**
160
     * Return the current element.
161
     *
162
     * @return mixed
163
     */
164
    public function current()
165
    {
166
        return $this->data->current();
167
    }
168
169
    /**
170
     * Move forward to next element.
171
     */
172
    public function next()
173
    {
174
        $this->data->next();
175
    }
176
177
    /**
178
     * Return the key of the current element.
179
     *
180
     * @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...
181
     */
182
    public function key()
183
    {
184
        return $this->data->key();
185
    }
186
187
    /**
188
     * Checks if current position is valid.
189
     *
190
     * @return boolean
191
     */
192
    public function valid()
193
    {
194
        return $this->data->valid();
195
    }
196
197
    /**
198
     * Rewind the Iterator to the first element.
199
     */
200
    public function rewind()
201
    {
202
        $this->data->rewind();
203
    }
204
205
    /**
206
     * Count elements of an object.
207
     *
208
     * @return int The custom count as an integer.
209
     */
210
    public function count()
211
    {
212
        return $this->data->count();
213
    }
214
215
    /**
216
     * Get item at collection.
217
     *
218
     * 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...
219
     *
220
     * @param int|string $offset Offset (index) to retrieve
221
     *
222
     * @return mixed
223
     */
224
    public function offsetGet($offset)
225
    {
226
        if (Str::contains($offset, static::SLICE_DELIM)) {
227
            return $this($offset)->toArray();
228
        }
229
        if ($offset < 0) {
230
            $offset = $this->count() + $offset;
231
        }
232
        return $this->data->offsetGet($offset);
233
    }
234
235
    /**
236
     * Set offset.
237
     *
238
     * Because Sequence is immutable, this operation is not allowed. Use set() instead.
239
     *
240
     * @param int $offset  Numeric offset
241
     * @param mixed $value Value
242
     */
243
    public function offsetSet($offset, $value)
244
    {
245
        throw new RuntimeException(sprintf(
246
            'Cannot set value on %s object.',
247
            __CLASS__
248
        ));
249
    }
250
251
    /**
252
     * Set value at given offset.
253
     *
254
     * 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...
255
     *
256
     * @param mixed $offset The index offset to set
257
     * @param mixed $value  The value to set it to
258
     *
259
     * @return $this
260
     */
261
    public function set($offset, $value)
262
    {
263
        $arr = $this->getData();
264
        $arr[$offset] = $value;
265
        return new static($arr);
266
    }
267
268
    /**
269
     *
270
     * Because Sequence is immutable, this operation is not allowed. Use set() instead.
271
     *
272
     * @param int $offset  Numeric offset
273
     */
274
    public function offsetUnset($offset)
275
    {
276
        throw new RuntimeException(sprintf(
277
            'Cannot unset value on %s object.',
278
            __CLASS__
279
        ));
280
    }
281
282
    /**
283
     * Get new sequence without specified indices.
284
     *
285
     * Creates a copy of the sequence, unsetting the specified offset(s) (on the copy), and returns it.
286
     *
287
     * @param int|string|array The offset, range, or set of indices to remove.
288
     *
289
     * @return $this
290
     */
291
    public function except($offset)
292
    {
293
        if (!is_array($offset)) {
294
            if (is_string($offset) && Str::contains($offset, static::SLICE_DELIM)) {
295
                list($start, $length) = get_range_start_end($offset, $this->count());
296
                $indices = array_slice($this->getData(), $start, $length, true);
297
            } else {
298
                $indices = array_flip([$offset]);
299
            }
300
        } else {
301
            $indices = array_flip($offset);
302
        }
303
        return $this->diffKeys($indices);
304
    }
305
306
    /**
307
     * Is there a value at specified offset?
308
     *
309
     * Returns true of there is an item in the collection at the specified numerical offset.
310
     *
311
     * @param mixed $offset The index offset to check
312
     *
313
     * @return bool
314
     */
315
    public function offsetExists($offset)
316
    {
317
        if ($offset < 0) {
318
            $offset = $this->count() + $offset;
319
        }
320
        return $this->data->offsetExists($offset);
321
    }
322
323
    /**
324
     * Get diff by index.
325
     *
326
     * @param array|Traversable$data The array/traversable
327
     *
328
     * @return static
329
     */
330
    public function diffKeys($data)
331
    {
332
        if (!is_array($data)) {
333
            $data = to_array($data);
334
        }
335
        return new static(array_diff_key(
336
            $this->getData(),
337
            $data
338
        ));
339
    }
340
341
    /**
342
     * Get diff by value.
343
     *
344
     * @param array|Traversable$data The array/traversable
345
     *
346
     * @return static
347
     */
348
    public function diff($data)
349
    {
350
        if (!is_array($data)) {
351
            $data = to_array($data);
352
        }
353
        return new static(array_diff(
354
            $this->getData(),
355
            $data
356
        ));
357
    }
358
359
    /**
360
     * Prepend item to collection.
361
     *
362
     * Prepend an item to this collection (in place).
363
     *
364
     * @param mixed $item Item to prepend to collection
365
     *
366
     * @return Sequence
367
     */
368
     public function prepend($item)
369
     {
370
         $arr = $this->getData();
371
         array_unshift($arr, $item);
372
         return new static($arr);
373
     }
374
375
    /**
376
     * Append item to collection.
377
     *
378
     * Append an item to this collection (in place).
379
     *
380
     * @param mixed $item Item to append to collection
381
     *
382
     * @return Sequence
383
     */
384
    public function append($item)
385
    {
386
        $arr = $this->getData();
387
        array_push($arr, $item);
388
        return new static($arr);
389
    }
390
391
    /**
392
     * Fold (reduce) sequence into a single value.
393
     *
394
     * @param callable $funk    A callback function
395
     * @param mixed    $initial Initial value for accumulator
396
     *
397
     * @return mixed
398
     */
399
    public function fold(callable $funk, $initial = null)
400
    {
401
        $carry = $initial;
402
        foreach ($this->getData() as $key => $val) {
403
            $carry = $funk($carry, $val, $key);
404
        }
405
        return $carry;
406
    }
407
408
    /**
409
     * Is collection empty?
410
     *
411
     * You may optionally pass in a callback which will determine if each of the items within the collection are empty.
412
     * If all items in the collection are empty according to this callback, this method will return true.
413
     *
414
     * @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...
415
     *
416
     * @return bool
417
     */
418
    public function isEmpty(callable $funk = null)
419
    {
420
        if (!is_null($funk)) {
421
            return $this->fold(function ($carry, $val) use ($funk) {
422
                return $carry && $funk($val);
423
            }, true);
424
        }
425
        return empty($this->data->toArray());
426
    }
427
428
    /**
429
     * Pipe collection through callback.
430
     *
431
     * Passes entire collection to provided callback and returns the result.
432
     *
433
     * @param callable $funk The callback funkshun
434
     *
435
     * @return mixed
436
     */
437
    public function pipe(callable $funk)
438
    {
439
        return $funk($this);
440
    }
441
442
    /**
443
     * Does every item return true?
444
     *
445
     * If callback is provided, this method will return true if all items in collection cause callback to return true.
446
     * Otherwise, it will return true if all items in the collection have a truthy value.
447
     *
448
     * @param callable|null $funk The callback
449
     *
450
     * @return bool
451
     */
452
    public function every(callable $funk = null)
453
    {
454
        return $this->fold(function($carry, $val, $key) use ($funk) {
455
            if (!$carry) {
456
                return false;
457
            }
458
            if (!is_null($funk)) {
459
                return $funk($val, $key);
460
            }
461
            return (bool) $val;
462
        }, true);
463
    }
464
465
    /**
466
     * Does every item return false?
467
     *
468
     * This method is the exact opposite of "all".
469
     *
470
     * @param callable|null $funk The callback
471
     *
472
     * @return bool
473
     */
474
    public function none(callable $funk = null)
475
    {
476
        return !$this->fold(function($carry, $val, $key) use ($funk) {
477
            if ($carry) {
478
                return true;
479
            }
480
            if (!is_null($funk)) {
481
                return $funk($val, $key);
482
            }
483
            return (bool) $val;
484
        }, false);
485
    }
486
487
    /**
488
     * Get first item.
489
     *
490
     * Retrieve the first item in the collection or, if a callback is provided, return the first item that, when passed
491
     * to the callback, returns true.
492
     *
493
     * @param callable|null $funk    The callback function
494
     * @param null          $default The default value
495
     *
496
     * @return mixed
497
     */
498
    public function first(callable $funk = null, $default = null)
499
    {
500
        if (is_null($funk) && $this->count()) {
501
            return $this[0];
502
        }
503
        foreach ($this as $key => $val) {
504
            if ($funk($val, $key)) {
505
                return $val;
506
            }
507
        }
508
        return $default;
509
    }
510
511
    /**
512
     * Get last item.
513
     *
514
     * Retrieve the last item in the collection or, if a callback is provided, return the last item that, when passed
515
     * to the callback, returns true.
516
     *
517
     * @param callable|null $funk    The callback function
518
     * @param null          $default The default value
519
     *
520
     * @return mixed
521
     */
522
    public function last(callable $funk = null, $default = null)
523
    {
524
        return $this->reverse()->first($funk, $default);
525
    }
526
527
    /**
528
     * Get sequence in reverse order.
529
     *
530
     * @return Sequenceable
531
     */
532
    public function reverse()
533
    {
534
        return new static(array_reverse($this->getData()));
535
    }
536
537
    /**
538
     * Return new sequence with the first item "bumped" off.
539
     *
540
     * @return Sequenceable
541
     */
542
    public function bump()
543
    {
544
        $arr = $this->getData();
545
        array_shift($arr);
546
        return new static($arr);
547
    }
548
549
    /**
550
     * Return new sequence with the last item "dropped" off.
551
     *
552
     * @return Sequenceable
553
     */
554
    public function drop()
555
    {
556
        $arr = $this->getData();
557
        array_pop($arr);
558
        return new static($arr);
559
    }
560
561
    public function unserialize($serialized)
562
    {
563
        $this->setData(unserialize($serialized));
564
    }
565
}
566