1 <?php namespace CupOfTea\Counter;
2
3 use Iterator;
4 use Countable;
5 use Traversable;
6 use ArrayIterator;
7 use SeekableIterator;
8 use IteratorAggregate;
9 use OutOfBoundsException;
10 use InvalidArgumentException;
11 use CupOfTea\Package\Package;
12
13 class Counter implements SeekableIterator
14 {
15 use Package;
16
17 /**
18 * Package Name.
19 *
20 * @const string
21 */
22 const PACKAGE = 'CupOfTea/Counter';
23
24 /**
25 * Package Version.
26 *
27 * @const string
28 */
29 const VERSION = '1.0.3';
30
31 /**
32 * The current position.
33 *
34 * @var int
35 */
36 private $i = 0;
37
38 /**
39 * The length of the traversable or counter.
40 *
41 * @var int
42 */
43 private $length;
44
45 /**
46 * The Traversable.
47 *
48 * @var \Iterator
49 */
50 private $traversable;
51
52 //
53 // Traversing counter
54 //
55
56 /**
57 * Loop over a variable.
58 *
59 * @param mixed $traversable
60 * @return \CupOfTea\Support\Counter
61 */
62 public function loop($traversable)
63 {
64 $this->clear();
65 $this->setTraversable($traversable);
66 $this->setLength($this->traversable);
67
68 return $this;
69 }
70
71 /**
72 * Resolve the variable we are looping over to something traversable.
73 *
74 * @param mixed $traversable
75 * @return void
76 */
77 private function setTraversable($traversable)
78 {
79 $traversable = value($traversable);
80
81 if ($traversable instanceof Iterator) {
82 $this->traversable = $traversable;
83 } elseif ($traversable instanceof IteratorAggregate) {
84 $this->traversable = $traversable->getIterator();
85 } elseif (class_exists('Illuminate\Contracts\Support\Arrayable') && $traversable instanceof \Illuminate\Contracts\Support\Arrayable) {
86 $this->traversable = new ArrayIterator($traversable->toArray());
87 } else {
88 $this->traversable = new ArrayIterator((array) $traversable);
89 }
90 }
91
92 /**
93 * Get the item that's being traversed.
94 *
95 * @return \Traversable|array
96 */
97 public function getTraversable()
98 {
99 if ($this->traversable instanceof ArrayIterator) {
100 $array = $this->traversable->getArrayCopy();
101
102 for ($i = 0; $i < $this->i; $i++) {
103 next($array);
104 }
105
106 return $array;
107 }
108
109 return $this->traversable;
110 }
111
112 /**
113 * Determine if a traversable is set.
114 *
115 * @return bool
116 */
117 private function traversable()
118 {
119 return isset($this->traversable);
120 }
121
122 /**
123 * Determine the length of a countable.
124 *
125 * @param mixed $countable
126 * @return void
127 */
128 private function setLength($countable)
129 {
130 $countable = value($countable);
131
132 if ($this->isInt($countable)) {
133 $this->length = (int) $countable;
134 } elseif (is_array($countable) || $countable instanceof Countable) {
135 $this->length = count($countable);
136 } elseif (class_exists('Illuminate\Contracts\Support\Arrayable') && $countable instanceof \Illuminate\Contracts\Support\Arrayable) {
137 $this->length = count($countable->toArray());
138 } elseif ($countable instanceof Traversable) {
139 $this->length = 0;
140
141 foreach ($countable as $v) {
142 $this->length++;
143 }
144 } else {
145 $this->length = INF;
146 }
147 }
148
149 //
150 // SeekableIterator implementation
151 //
152
153 /**
154 * Seeks to a position.
155 *
156 * @param int $position
157 * @return void
158 * @throws \InvalidArgumentException when $position is not an integer.
159 * @throws \OutOfBoundsException when the seek $position exeeds the traversable's length.
160 */
161 public function seek($position)
162 {
163 if (! $this->isInt($position)) {
164 throw new InvalidArgumentException('Seek position must be an integer.');
165 }
166
167 $position = max(0, (int) $position);
168
169 if ($this->length !== null && $this->length < $position) {
170 throw new OutOfBoundsException('Invalid seek position (' . $position . ').');
171 }
172
173 if ($this->traversable()) {
174 if ($this->traversable instanceof SeekableIterator) {
175 $this->traversable->seek($position);
176 } else {
177 $this->traversable->rewind();
178
179 for ($i = 0; $i < $position; $i++) {
180 $this->traversable->next();
181 }
182 }
183 }
184
185 $this->i = $position;
186 }
187
188 /**
189 * Set the internal pointer of the traversable to its first element.
190 *
191 * @return mixed
192 */
193 public function rewind()
194 {
195 $this->i = 0;
196
197 if ($this->traversable()) {
198 $this->traversable->rewind();
199
200 return $this->length !== 0 ? $this->traversable->current() : false;
201 }
202
203 return $this->i;
204 }
205
206 /**
207 * Return the current element in the traversable.
208 *
209 * @return mixed
210 */
211 public function current()
212 {
213 if ($this->traversable()) {
214 return $this->traversable->current();
215 }
216
217 return $this->i;
218 }
219
220 /**
221 * Return the index element of the current traversable position.
222 *
223 * @return mixed
224 */
225 public function key()
226 {
227 if ($this->traversable()) {
228 return $this->traversable->key();
229 }
230
231 return $this->i;
232 }
233
234 /**
235 * Advance the internal array pointer of the traversable.
236 *
237 * @return mixed
238 */
239 public function next()
240 {
241 $this->i++;
242
243 if ($this->traversable()) {
244 $this->traversable->next();
245
246 return $this->traversable->current();
247 }
248
249 return $this->i;
250 }
251
252 /**
253 * Rewind the internal array pointer of the traversable.
254 *
255 * @return mixed
256 */
257 public function prev()
258 {
259 $this->i = max(0, $this->i - 1);
260
261 if ($this->traversable()) {
262 $this->seek($this->i);
263
264 return $this->traversable->current();
265 }
266
267 return $this->i;
268 }
269
270 /**
271 * Checks if current position of the traversable is valid.
272 *
273 * @return bool
274 */
275 public function valid()
276 {
277 if ($this->traversable()) {
278 $key = $this->traversable->key();
279
280 // The is_null check is a safetyguard to make sure we don't end up
281 // in an infinite loop if some idiot decided to use null as a key.
282 return ! is_null($key) && $this->traversable->valid();
283 }
284
285 return $this->i < $this->length;
286 }
287
288 /**
289 * Set the internal pointer of the traversable to its last element.
290 *
291 * @return mixed
292 */
293 public function end()
294 {
295 if ($this->traversable()) {
296 if ($this->length !== 0) {
297 $this->seek($this->length);
298
299 return $this->traversable->current();
300 }
301
302 return false;
303 }
304
305 return ! is_null($this->length) ? $this->length : false;
306 }
307
308 //
309 // Simple counter
310 //
311
312 /**
313 * Start a simple counter.
314 *
315 * @param bool|int $length
316 * @return void
317 */
318 public function start($length = false)
319 {
320 $this->clear();
321 $this->setLength($length);
322
323 return $this;
324 }
325
326 //
327 // Counter methods
328 //
329
330 /**
331 * Increment the counter by a specified amount.
332 *
333 * @param int $by
334 * @return void
335 */
336 public function increment($by = 1)
337 {
338 switch ($by) {
339 case 0:
340 break;
341 case 1:
342 $this->next();
343 break;
344 default:
345 $seek = $this->traversable() ? min($this->length - 1, $this->i + $by) : $this->i + $by;
346
347 $this->seek($seek);
348 break;
349 }
350 }
351
352 /**
353 * Decrement the counter by a specified amount.
354 *
355 * @param int $by
356 * @return void
357 */
358 public function decrement($by = 1)
359 {
360 switch ($by) {
361 case 0:
362 break;
363 case 1:
364 $this->prev();
365 break;
366 default:
367 $this->seek(max(0, $this->i - $by));
368 break;
369 }
370 }
371
372 /**
373 * Increment the counter by 1.
374 *
375 * @return void
376 */
377 public function tick()
378 {
379 $this->increment();
380 }
381
382 /**
383 * Check if the current position is the initial position.
384 *
385 * @return bool
386 */
387 public function first()
388 {
389 return $this->i == 0;
390 }
391
392 /**
393 * Check if the current position is the last position.
394 * Will always return false if no length was
395 * specified for a simple counter.
396 *
397 * @return bool
398 */
399 public function last()
400 {
401 return $this->length !== null && $this->i + 1 >= $this->length;
402 }
403
404 /**
405 * Check if the current iteration is the nth iteration (1 based).
406 *
407 * @return bool
408 */
409 public function nth($n)
410 {
411 return $this->iteration() % $n == 0;
412 }
413
414 /**
415 * Check if the current iteration is an even iteration (1 based).
416 *
417 * @return bool
418 */
419 public function even()
420 {
421 return $this->nth(2);
422 }
423
424 /**
425 * Check if the current iteration is an odd iteration (1 based).
426 *
427 * @return bool
428 */
429 public function odd()
430 {
431 return ! $this->even();
432 }
433
434 /**
435 * Return the current element.
436 *
437 * @return mixed
438 */
439 public function item()
440 {
441 if ($this->traversable()) {
442 return $this->current();
443 }
444 }
445
446 /**
447 * Get the current position.
448 *
449 * @return int
450 */
451 public function index()
452 {
453 return $this->i;
454 }
455
456 /**
457 * Get the current iteration.
458 *
459 * @return int
460 */
461 public function iteration()
462 {
463 return $this->i + 1;
464 }
465
466 /**
467 * Get the length of the traversable or the counter.
468 *
469 * @return float
470 */
471 public function length()
472 {
473 return $this->length;
474 }
475
476 /**
477 * Reset the counter to its default state.
478 *
479 * @return void
480 */
481 private function clear()
482 {
483 $this->i = 0;
484 $this->length = $this->traversable = null;
485 }
486
487 //
488 // helper methods
489 //
490
491 /**
492 * Check if a variable is castable to an integer.
493 *
494 * @param mixed $int
495 * @return bool
496 */
497 private function isInt($int)
498 {
499 return (is_numeric($int) && (int) $int == (float) $int) || is_null($int);
500 }
501
502 //
503 // method aliases
504 //
505
506 /**
507 * @see \CupOfTea\Support\Counter::loop
508 */
509 public function traverse($traversable)
510 {
511 return $this->loop($traversable);
512 }
513
514 /**
515 * @see \CupOfTea\Support\Counter::index
516 */
517 public function i()
518 {
519 return $this->index();
520 }
521
522 /**
523 * @see \CupOfTea\Support\Counter::index
524 */
525 public function position()
526 {
527 return $this->index();
528 }
529 }
530