Completed
Pull Request — master (#16)
by James
04:35
created

Collection::count_while_true()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
crap 3
1
<?php
2
namespace Intraxia\Jaxion\Axolotl;
3
4
use Intraxia\Jaxion\Contract\Axolotl\Collection as CollectionContract;
5
use Intraxia\Jaxion\Contract\Axolotl\Serializes;
6
use InvalidArgumentException;
7
use OutOfBoundsException;
8
use OutOfRangeException;
9
10
/**
11
 * Class Collection
12
 *
13
 * @package Intraxia\Jaxion
14
 * @subpackage Axolotl
15
 */
16
class Collection implements CollectionContract {
17
18
	/**
19
	 * Collection elements.
20
	 *
21
	 * @var array
22
	 */
23
	protected $elements = array();
24
25
	/**
26
	 * Collection type to enforce.
27
	 *
28
	 * @var Type
29
	 */
30
	private $type;
31
32
	/**
33
	 * Where Collection is in loop.
34
	 *
35
	 * @var int
36
	 */
37
	protected $position = 0;
38
39
	/**
40
	 * Collection constructor.
41
	 *
42
	 * @param string $type
43
	 * @param array  $elements
44
	 */
45 138
	public function __construct( $type, array $elements = array() ) {
46 138
		$this->type = new Type( $type );
47
48 135
		if ( $this->type->is_model() ) {
49 33
			foreach ( $elements as $idx => $element ) {
50 27
				if ( is_array( $element ) ) {
51 9
					$elements[ $idx ] = $this->type->create_model( $element );
52 9
				}
53 33
			}
54 33
		}
55
56 135
		if ( $elements ) {
57 66
			$this->type->validate_elements( $elements );
58 66
		}
59
60 135
		$this->elements = $elements;
61 135
	}
62
63
	/**
64
	 * {@inheritdoc}
65
	 *
66
	 * @return string
67
	 */
68 78
	public function get_type() {
69 78
		return $this->type->get_type();
70
	}
71
72
	/**
73
	 * {@inheritdoc}
74
	 *
75
	 * @param mixed $element
76
	 *
77
	 * @return Collection
78
	 *
79
	 * @throws InvalidArgumentException
80
	 */
81 51
	public function add( $element ) {
82 51
		if ( $this->type->is_model() && is_array( $element ) ) {
83 3
			$element = $this->type->create_model( $element );
84 3
		}
85
86 51
		$this->type->validate_element( $element );
87
88 45
		$elements   = $this->elements;
89 45
		$elements[] = $element;
90
91 45
		$collection = new static( $this->get_type() );
92 45
		$collection->set_from_trusted( $elements );
93
94 45
		return $collection;
95
	}
96
97
	/**
98
	 * {@inheritdoc}
99
	 *
100
	 * @return Collection
101
	 */
102 3
	public function clear() {
103 3
		return new static( $this->get_type() );
104
	}
105
106
	/**
107
	 * {@inheritdoc}
108
	 *
109
	 * @param  callable $condition Condition to satisfy.
110
	 *
111
	 * @return bool
112
	 */
113 6
	public function contains( $condition ) {
114 6
		return (bool) $this->find( $condition );
115
	}
116
117
	/**
118
	 * {@inheritdoc}
119
	 *
120
	 * @param  callable $condition Condition to satisfy.
121
	 *
122
	 * @return mixed
123
	 */
124 6
	public function find( $condition ) {
125 6
		$index = $this->find_index( $condition );
126
127 6
		return -1 === $index ? false : $this->elements[ $index ];
128
	}
129
130
	/**
131
	 * {@inheritdoc}
132
	 *
133
	 * @param  callable $condition Condition to satisfy.
134
	 *
135
	 * @return int
136
	 */
137 6
	public function find_index( $condition ) {
138 6
		$index = -1;
139
140 6
		for ( $i = 0, $count = count( $this->elements ); $i < $count; $i++ ) {
141 6
			if ( call_user_func( $condition, ($this->at( $i ) ) ) ) {
142 3
				$index = $i;
143 3
				break;
144
			}
145 3
		}
146
147 6
		return $index;
148
	}
149
150
	/**
151
	 * Fetches the element at the provided index.
152
	 *
153
	 * @param int $index Index to get element from.
154
	 *
155
	 * @return mixed
156
	 *
157
	 * @throws OutOfRangeException
158
	 */
159 45
	public function at( $index ) {
160 45
		$this->validate_index( $index );
161
162 36
		return $this->elements[ $index ];
163
	}
164
165
	/**
166
	 * {@inheritdoc}
167
	 *
168
	 * @param  int $index Index to check for existence.
169
	 *
170
	 * @return bool
171
	 *
172
	 * @throws InvalidArgumentException
173
	 */
174 45
	public function index_exists( $index ) {
175 45
		if ( ! is_int( $index ) ) {
176 3
			throw new InvalidArgumentException( 'Index must be an integer' );
177
		}
178
179 42
		if ( $index < 0 ) {
180 3
			throw new InvalidArgumentException( 'Index must be a non-negative integer' );
181
		}
182
183 39
		return $index < $this->count();
184
	}
185
186
	/**
187
	 * {@inheritdoc}
188
	 *
189
	 * @param  callable $condition Condition to satisfy.
190
	 *
191
	 * @return mixed
192
	 */
193
	public function filter( $condition ) {
194
		$elements = array();
195
196
		foreach ( $this->elements as $element ) {
197
			if ( call_user_func( $condition, $element ) ) {
198
				$elements[] = $element;
199
			}
200
		}
201
202
		return $this->new_from_trusted( $elements );
203
	}
204
	/**
205
	 * {@inheritdoc}
206
	 *
207
	 * @param  callable $condition Condition to satisfy.
208
	 *
209
	 * @return mixed
210
	 */
211
	public function find_last( $condition ) {
212
		$index = $this->find_last_index( $condition );
213
214
		return -1 === $index ? null : $this->elements[ $index ];
215
	}
216
217
	/**
218
	 * {@inheritdoc}
219
	 *
220
	 * @param  callable $condition
221
	 * @return int
222
	 */
223
	public function find_last_index( $condition ) {
224
		$index = -1;
225
226
		for ( $i = count( $this->elements ) - 1; $i >= 0; $i-- ) {
227
			if ( call_user_func( $condition, $this->elements[ $i ] ) ) {
228
				$index = $i;
229
				break;
230
			}
231
		}
232
233
		return $index;
234
	}
235
236
	/**
237
	 * {@inheritdoc}
238
	 *
239
	 * @param  int $start Begining index to slice from.
240
	 * @param  int $end   End index to slice to.
241
	 *
242
	 * @return Collection
243
	 *
244
	 * @throws InvalidArgumentException
245
	 */
246 27
	public function slice( $start, $end ) {
247 27
		if ( $start < 0 || ! is_int( $start ) ) {
248
			throw new InvalidArgumentException( 'Start must be a non-negative integer' );
249
		}
250
251 27
		if ( $end < 0 || ! is_int( $end ) ) {
252
			throw new InvalidArgumentException( 'End must be a positive integer' );
253
		}
254
255 27
		if ( $start > $end ) {
256 3
			throw new InvalidArgumentException( 'End must be greater than start' );
257
		}
258
259 27
		if ( $end > $this->count() + 1 ) {
260
			throw new InvalidArgumentException( 'End must be less than the count of the items in the Collection' );
261
		}
262
263 27
		$length = $end - $start + 1;
264
265 27
		return $this->new_from_trusted( array_slice( $this->elements, $start, $length ) );
266
	}
267
268
	/**
269
	 * {@inheritdoc}
270
	 *
271
	 * @param int   $index     Index to start at.
272
	 * @param mixed $element Element to insert.
273
	 *
274
	 * @return Collection
275
	 *
276
	 * @throws InvalidArgumentException
277
	 * @throws OutOfRangeException
278
	 */
279
	public function insert( $index, $element ) {
280
		$this->validate_index( $index );
281
		$this->type->validate_element( $element );
282
283
		$a = array_slice( $this->elements, 0, $index );
284
		$b = array_slice( $this->elements, $index, count( $this->elements ) );
285
286
		$a[] = $element;
287
288
		return $this->new_from_trusted( array_merge( $a, $b ) );
289
	}
290
291
	/**
292
	 * {@inheritdoc}
293
	 *
294
	 * @param int   $index    Index to start insertion at.
295
	 * @param array $elements Elements in insert.
296
	 *
297
	 * @return Collection
298
	 *
299
	 * @throws OutOfRangeException
300
	 */
301
	public function insert_range( $index, array $elements ) {
302
		$this->validate_index( $index );
303
		$this->type->validate_elements( $elements );
304
305
		if ( $index < 0 ) {
306
			$index = $this->count() + $index + 1;
307
		}
308
309
		return $this->new_from_trusted(
310
			array_merge(
311
				array_slice( $this->elements, 0, $index ),
312
				$elements,
313
				array_slice( $this->elements, $index, count( $this->elements ) )
314
			)
315
		);
316
	}
317
318
	/**
319
	 * {@inheritdoc}
320
	 *
321
	 * @param  callable $condition Condition to satisfy.
322
	 *
323
	 * @return Collection
324
	 */
325
	public function without( $condition ) {
326
		$inverse = function ( $element ) use ( $condition ) {
327
			return ! call_user_func( $condition, $element );
328
		};
329
330
		return $this->filter( $inverse );
331
	}
332
333
	/**
334
	 * {@inheritdoc}
335
	 *
336
	 * @param  int $index Index to remove.
337
	 *
338
	 * @return Collection
339
	 *
340
	 * @throws OutOfRangeException
341
	 */
342
	public function remove_at( $index ) {
343
		$this->validate_index( $index );
344
345
		$elements = $this->elements;
346
347
		return $this->new_from_trusted(
348
			array_merge(
349
				array_slice( $elements, 0, $index ),
350
				array_slice( $elements, $index + 1, count( $elements ) )
351
			)
352
		);
353
	}
354
	/**
355
	 * {@inheritdoc}
356
	 *
357
	 * @return Collection
358
	 */
359
	public function reverse() {
360
		return $this->new_from_trusted(
361
			array_reverse( $this->elements )
362
		);
363
	}
364
365
	/**
366
	 * {@inheritdoc}
367
	 *
368
	 * @param callable $callback Sort callback.
369
	 *
370
	 * @return Collection
371
	 */
372
	public function sort( $callback ) {
373
		$elements = $this->elements;
374
		usort( $elements, $callback );
375
		return $this->new_from_trusted( $elements );
376
	}
377
378
	/**
379
	 * {@inheritdoc}
380
	 *
381
	 * @return array
382
	 */
383 9
	public function to_array() {
384 9
		return $this->elements;
385
	}
386
387
	/**
388
	 * {@inheritdoc}
389
	 *
390
	 * @param callable $callable Reducer function.
391
	 *
392
	 * @param null     $initial  Initial reducer value.
393
	 *
394
	 * @return mixed
395
	 */
396
	public function reduce( $callable, $initial = null ) {
397
		return array_reduce( $this->elements, $callable, $initial );
398
	}
399
400
	/**
401
	 * {@inheritdoc}
402
	 *
403
	 * @param callable $condition Condition callback.
404
	 *
405
	 * @return bool
406
	 */
407
	public function every( $condition ) {
408
		$response = true;
409
410
		foreach ( $this->elements as $element ) {
411
			$result = call_user_func( $condition, $element );
412
413
			if ( false === $result ) {
414
				$response = false;
415
				break;
416
			}
417
		}
418
419
		return $response;
420
	}
421
422
	/**
423
	 * {@inheritdoc}
424
	 *
425
	 * @param  int $num Number of elements to drop.
426
	 *
427
	 * @return Collection
428
	 *
429
	 * @throws InvalidArgumentException
430
	 */
431 18
	public function drop( $num ) {
432 18
		return $this->slice( $num, $this->count() );
433
	}
434
435
	/**
436
	 * {@inheritdoc}
437
	 *
438
	 * @param int $num Number of elements to drop.
439
	 *
440
	 * @return Collection
441
	 *
442
	 * @throws InvalidArgumentException
443
	 */
444 9
	public function drop_right( $num ) {
445 9
		return $num !== $this->count()
446 9
			? $this->slice( 0, $this->count() - $num - 1 )
447 9
			: $this->clear();
448
	}
449
450
	/**
451
	 * {@inheritdoc}
452
	 *
453
	 * @param callable $condition Condition callback.
454
	 *
455
	 * @return Collection
456
	 */
457 9
	public function drop_while( $condition ) {
458 9
		$count = $this->count_while_true( $condition );
459 9
		return $count ? $this->drop( $count ) : $this;
460
	}
461
	/**
462
	 * {@inheritdoc}
463
	 *
464
	 * @return Collection
465
	 *
466
	 * @throws InvalidArgumentException
467
	 */
468
	public function tail() {
469
		return $this->slice( 1, $this->count() );
470
	}
471
472
	/**
473
	 * {@inheritdoc}
474
	 *
475
	 * @param  int $num Number of elements to take.
476
	 *
477
	 * @return Collection
478
	 *
479
	 * @throws InvalidArgumentException
480
	 */
481
	public function take( $num ) {
482
		return $this->slice( 0, $num - 1 );
483
	}
484
485
	/**
486
	 * {@inheritdoc}
487
	 *
488
	 * @param int $num Number of elements to take.
489
	 *
490
	 * @return Collection
491
	 *
492
	 * @throws InvalidArgumentException
493
	 */
494
	public function take_right( $num ) {
495
		return $this->slice( $this->count() - $num, $this->count() );
496
	}
497
498
	/**
499
	 * {@inheritdoc}
500
	 *
501
	 * @param callable $condition Callback function.
502
	 *
503
	 * @return Collection
504
	 */
505
	public function take_while( $condition ) {
506
		$count = $this->count_while_true( $condition );
507
508
		return $count ? $this->take( $count ) : $this->clear();
509
	}
510
511
	/**
512
	 * {@inheritdoc}
513
	 *
514
	 * @param callable $callable Callback function.
515
	 */
516
	public function each( $callable ) {
517
		foreach ( $this->elements as $element ) {
518
			call_user_func( $callable, $element );
519
		}
520
	}
521
522
	/**
523
	 * {@inheritdoc}
524
	 *
525
	 * @param callable $callable Callback function.
526
	 *
527
	 * @return Collection
528
	 */
529 9
	public function map( $callable ) {
530 9
		$elements = array();
531 9
		$type = null;
532 9
		foreach ( $this->elements as $element ) {
533 6
			$result = call_user_func( $callable, $element );
534
535 6
			if ( null === $type ) {
536 6
				$type = gettype( $result );
537
538 6
				if ( 'object' === $type ) {
539
					$type = get_class( $result );
540
				}
541 6
			}
542
543 6
			$elements[] = $result;
544 9
		}
545
546 9
		return $this->new_from_trusted( $elements, $type ? : $this->get_type() );
547
	}
548
549
	/**
550
	 * {@inheritdoc}
551
	 *
552
	 * @param callable $callable Reducer function.
553
	 * @param null     $initial  Initial value.
554
	 *
555
	 * @return mixed
556
	 */
557
	public function reduce_right( $callable, $initial = null ) {
558
		return array_reduce(
559
			array_reverse( $this->elements ),
560
			$callable,
561
			$initial
562
		);
563
	}
564
565
	/**
566
	 * {@inheritdoc}
567
	 *
568
	 * @return Collection
569
	 */
570
	public function shuffle() {
571
		$elements = $this->elements;
572
		shuffle( $elements );
573
574
		return $this->new_from_trusted( $elements );
575
	}
576
577
	/**
578
	 * {@inheritdoc}
579
	 *
580
	 * @param array|Collection $elements Array of elements to merge.
581
	 *
582
	 * @return Collection
583
	 *
584
	 * @throws InvalidArgumentException
585
	 */
586
	public function merge( $elements ) {
587
		if ( $elements instanceof static ) {
588
			$elements = $elements->to_array();
589
		}
590
591
		if ( ! is_array( $elements ) ) {
592
			throw new InvalidArgumentException( 'Merge must be given array or Collection' );
593
		}
594
595
		$this->type->validate_elements( $elements );
596
597
		return $this->new_from_trusted(
598
			array_merge( $this->elements, $elements )
599
		);
600
	}
601
602
	/**
603
	 * {@inheritdoc}
604
	 *
605
	 * @return mixed
606
	 *
607
	 * @throws OutOfBoundsException
608
	 */
609
	public function first() {
610
		if ( empty( $this->elements ) ) {
611
			throw new OutOfBoundsException( 'Cannot get first element of empty Collection' );
612
		}
613
614
		return reset( $this->elements );
615
	}
616
617
	/**
618
	 * {@inheritdoc}
619
	 *
620
	 * @return mixed
621
	 *
622
	 * @throws OutOfBoundsException
623
	 */
624
	public function last() {
625
		if ( empty( $this->elements ) ) {
626
			throw new OutOfBoundsException( 'Cannot get last element of empty Collection' );
627
		}
628
629
		return end( $this->elements );
630
	}
631
632
	/**
633
	 * {@inheritdoc}
634
	 *
635
	 * @return int
636
	 */
637 90
	public function count() {
638 90
		return count( $this->elements );
639
	}
640
641
	/**
642
	 * {@inheritDoc}
643
	 *
644
	 * @return array
645
	 */
646
	public function serialize() {
647 9
		return $this->map(function( $element ) {
648 6
			if ( $element instanceof Serializes ) {
649 3
				return $element->serialize();
650
			}
651
652 3
			return $element;
653 9
		} )->to_array();
654
	}
655
656
	/**
657
	 * Return the current element.
658
	 *
659
	 * @return mixed
660
	 */
661 3
	public function current() {
662 3
		return $this->at( $this->position );
663
	}
664
665
	/**
666
	 * Move forward to next element.
667
	 */
668 3
	public function next() {
669 3
		$this->position ++;
670 3
	}
671
672
	/**
673
	 * Return the key of the current element.
674
	 *
675
	 * @return mixed
676
	 */
677 3
	public function key() {
678 3
		return $this->position;
679
	}
680
681
	/**
682
	 * Checks if current position is valid.
683
	 *
684
	 * @return bool
685
	 */
686 3
	public function valid() {
687 3
		return isset( $this->elements[ $this->position ] );
688
	}
689
690
	/**
691
	 * Rewind the Iterator to the first element.
692
	 */
693 3
	public function rewind() {
694 3
		$this->position = 0;
695 3
	}
696
697
	/**
698
	 * Creates a new instance of the Collection
699
	 * from a trusted set of elements.
700
	 *
701
	 * @param array      $elements Array of elements to pass into new collection.
702
	 * @param null|mixed $type
703
	 *
704
	 * @return static
705
	 */
706 36
	protected function new_from_trusted( array $elements, $type = null ) {
707 36
		$collection = new static( null !== $type ? $type : $this->get_type() );
708 36
		$collection->set_from_trusted( $elements );
709
710 36
		return $collection;
711
	}
712
713
	/**
714
	 * Sets the elements without validating them.
715
	 *
716
	 * @param array $elements Pre-validated elements to set.
717
	 */
718 81
	protected function set_from_trusted( array $elements ) {
719 81
		$this->elements = $elements;
720 81
	}
721
722
	/**
723
	 * Number of elements true for the condition.
724
	 *
725
	 * @param callable $condition Condition to check.
726
	 * @return int
727
	 */
728 9
	protected function count_while_true( $condition ) {
729 9
		$count = 0;
730
731 9
		foreach ( $this->elements as $element ) {
732 9
			if ( ! $condition($element) ) {
733 6
				break;
734
			}
735 6
			$count++;
736 9
		}
737
738 9
		return $count;
739
	}
740
741
	/**
742
	 * Validates a number to be used as an index.
743
	 *
744
	 * @param  integer $index The number to be validated as an index.
745
	 *
746
	 * @throws OutOfRangeException
747
	 */
748 45
	protected function validate_index( $index ) {
749 45
		$exists = $this->index_exists( $index );
750
751 39
		if ( ! $exists ) {
752 3
			throw new OutOfRangeException( 'Index out of bounds of collection' );
753
		}
754 36
	}
755
}
756