Completed
Pull Request — master (#49)
by Luke
04:43 queued 02:20
created

Sequence   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 501
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 0%

Importance

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