Passed
Push — features/47-laravelmethods ( baa4b5...734d23 )
by Luke
02:25
created

Sequence::toSeq()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 2
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
        return $this->data->offsetExists($offset);
305
    }
306
307
    /**
308
     * Get diff by index.
309
     *
310
     * @param array|Traversable$data The array/traversable
311
     *
312
     * @return static
313
     */
314
    public function diffKeys($data)
315
    {
316
        if (!is_array($data)) {
317
            $data = to_array($data);
318
        }
319
        return new static(array_diff_key(
320
            $this->getData(),
321
            $data
322
        ));
323
    }
324
325
    /**
326
     * Get diff by value.
327
     *
328
     * @param array|Traversable$data The array/traversable
329
     *
330
     * @return static
331
     */
332
    public function diff($data)
333
    {
334
        if (!is_array($data)) {
335
            $data = to_array($data);
336
        }
337
        return new static(array_diff(
338
            $this->getData(),
339
            $data
340
        ));
341
    }
342
343
    /**
344
     * Prepend item to collection.
345
     *
346
     * Prepend an item to this collection (in place).
347
     *
348
     * @param mixed $item Item to prepend to collection
349
     *
350
     * @return Sequence
351
     */
352
     public function prepend($item)
353
     {
354
         $arr = $this->getData();
355
         array_unshift($arr, $item);
356
         return new static($arr);
357
     }
358
359
    /**
360
     * Append item to collection.
361
     *
362
     * Append an item to this collection (in place).
363
     *
364
     * @param mixed $item Item to append to collection
365
     *
366
     * @return Sequence
367
     */
368
    public function append($item)
369
    {
370
        $arr = $this->getData();
371
        array_push($arr, $item);
372
        return new static($arr);
373
    }
374
375
    /**
376
     * Fold (reduce) sequence into a single value.
377
     *
378
     * @param callable $funk    A callback function
379
     * @param mixed    $initial Initial value for accumulator
380
     *
381
     * @return mixed
382
     */
383
    public function fold(callable $funk, $initial = null)
384
    {
385
        $carry = $initial;
386
        foreach ($this->getData() as $key => $val) {
387
            $carry = $funk($carry, $val, $key);
388
        }
389
        return $carry;
390
    }
391
392
    /**
393
     * Is collection empty?
394
     *
395
     * You may optionally pass in a callback which will determine if each of the items within the collection are empty.
396
     * If all items in the collection are empty according to this callback, this method will return true.
397
     *
398
     * @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...
399
     *
400
     * @return bool
401
     */
402
    public function isEmpty(callable $funk = null)
403
    {
404
        if (!is_null($funk)) {
405
            return $this->fold(function ($carry, $val) use ($funk) {
406
                return $carry && $funk($val);
407
            }, true);
408
        }
409
        return empty($this->data->toArray());
410
    }
411
412
    /**
413
     * Pipe collection through callback.
414
     *
415
     * Passes entire collection to provided callback and returns the result.
416
     *
417
     * @param callable $funk The callback funkshun
418
     *
419
     * @return mixed
420
     */
421
    public function pipe(callable $funk)
422
    {
423
        return $funk($this);
424
    }
425
426
    /**
427
     * Does every item return true?
428
     *
429
     * If callback is provided, this method will return true if all items in collection cause callback to return true.
430
     * Otherwise, it will return true if all items in the collection have a truthy value.
431
     *
432
     * @param callable|null $funk The callback
433
     *
434
     * @return bool
435
     */
436
    public function every(callable $funk = null)
437
    {
438
        return $this->fold(function($carry, $val, $key) use ($funk) {
439
            if (!$carry) {
440
                return false;
441
            }
442
            if (!is_null($funk)) {
443
                return $funk($val, $key);
444
            }
445
            return (bool) $val;
446
        }, true);
447
    }
448
449
    /**
450
     * Does every item return false?
451
     *
452
     * This method is the exact opposite of "all".
453
     *
454
     * @param callable|null $funk The callback
455
     *
456
     * @return bool
457
     */
458
    public function none(callable $funk = null)
459
    {
460
        return !$this->fold(function($carry, $val, $key) use ($funk) {
461
            if ($carry) {
462
                return true;
463
            }
464
            if (!is_null($funk)) {
465
                return $funk($val, $key);
466
            }
467
            return (bool) $val;
468
        }, false);
469
    }
470
471
    /**
472
     * Get first item.
473
     *
474
     * Retrieve the first item in the collection or, if a callback is provided, return the first item that, when passed
475
     * to the callback, returns true.
476
     *
477
     * @param callable|null $funk    The callback function
478
     * @param null          $default The default value
479
     *
480
     * @return mixed
481
     */
482
    public function first(callable $funk = null, $default = null)
483
    {
484
        if (is_null($funk) && $this->count()) {
485
            return $this[0];
486
        }
487
        foreach ($this as $key => $val) {
488
            if ($funk($val, $key)) {
489
                return $val;
490
            }
491
        }
492
        return $default;
493
    }
494
495
    /**
496
     * Get last item.
497
     *
498
     * Retrieve the last item in the collection or, if a callback is provided, return the last item that, when passed
499
     * to the callback, returns true.
500
     *
501
     * @param callable|null $funk    The callback function
502
     * @param null          $default The default value
503
     *
504
     * @return mixed
505
     */
506
    public function last(callable $funk = null, $default = null)
507
    {
508
        return $this->reverse()->first($funk, $default);
509
    }
510
511
    /**
512
     * Get sequence in reverse order.
513
     *
514
     * @return Sequenceable
515
     */
516
    public function reverse()
517
    {
518
        return new static(array_reverse($this->getData()));
519
    }
520
521
    /**
522
     * Return new sequence with the first item "bumped" off.
523
     *
524
     * @return Sequenceable
525
     */
526
    public function bump()
527
    {
528
        $arr = $this->getData();
529
        array_shift($arr);
530
        return new static($arr);
531
    }
532
533
    /**
534
     * Return new sequence with the last item "dropped" off.
535
     *
536
     * @return Sequenceable
537
     */
538
    public function drop()
539
    {
540
        $arr = $this->getData();
541
        array_pop($arr);
542
        return new static($arr);
543
    }
544
}
545