Completed
Pull Request — master (#49)
by Luke
05:51 queued 02:32
created

Sequence   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 500
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 0
loc 500
rs 6.8
c 0
b 0
f 0
wmc 55
lcom 1
cbo 5

30 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
B __invoke() 0 17 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

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\Collection;
11
12
use BadMethodCallException;
13
use Noz\Traits\IsArrayable;
14
use RuntimeException;
15
16
use Iterator;
17
use ArrayAccess;
18
use Countable;
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
32
use function
33
    Noz\to_array,
34
    Noz\is_traversable,
35
    Noz\get_range_start_end;
36
37
/**
38
 * Sequence Collection.
39
 *
40
 * A sequence is a collection with consecutive, numeric indexes, starting from zero. It is immutable, and so any
41
 * operation that requires a change to its state will return a new sequence with whatever changes were intended.
42
 * The fact that this type of collection is indexed in this way allows some very convenient and useful functionality.
43
 * For instance, you can treat a sequence as if it were a regular array, using square brackets. Unlike a regular array
44
 * 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
45
 * "$start:$end" to retrieve a "slice" of the sequence.
46
 */
47
class Sequence implements
48
    Sequenceable,
49
    ArrayAccess,
50
    Immutable,
51
    Countable,
52
    Arrayable,
53
    Invokable,
54
    Iterator
55
{
56
    use IsImmutable, IsContainer, IsArrayable;
57
58
    /**
59
     * Delimiter used to fetch slices.
60
     */
61
    const SLICE_DELIM = ':';
62
63
    /**
64
     * Fixed-size data storage array.
65
     *
66
     * @var SplFixedArray
67
     */
68
    private $data;
69
70
    /**
71
     * Sequence constructor.
72
     *
73
     * @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...
74
     */
75
    public function __construct($data = null)
76
    {
77
        if (is_null($data)) {
78
            $data = [];
79
        }
80
        $this->setData($data);
81
    }
82
83
    /**
84
     * Invoke sequence.
85
     *
86
     * A sequence is invokable as if it were a function. This allows some pretty useful functionality such as negative
87
     * indexing, sub-sequence selection, etc.
88
     *
89
     * @param int $offset The offset to return
0 ignored issues
show
Documentation introduced by
Should the type for parameter $offset not be integer|null?

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.

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

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