PHP - How to count a generators yields
If you have to do it, following as a on-liner of native functions:
count(iterator_to_array($generator, false));
However, take care: After this your $generator
is executed and consumed. So if you would put that same $generator
into a foreach
in a following line, it would loop 0 times.
Generators are by design highly dynamic (in contrast to fixed data structures like arrays), thats why they don't offer ->count()
or ->rewind()
.
Actually, it depends in which case you are :
Case 1 : I can't count before iterating and I care about values
// The plain old solution
$count = 0;
foreach($traversable as $value) {
// Do something with $value, then…
++$count;
}
Case 2 : I can't count before iterating but I don't care about values
// let's iterator_count() do it for me
$count = iterator_count($traversable);
Case 3 : I can count before iterating but I don't care about values
I try not to use generators.
For example (with SQL backends) :
SELECT count(1) FROM mytable; // then return result
is better than
SELECT * FROM mytable; // then counting results
Other example (with xrange from Alma Do) :
// More efficient than counting by iterating
function count_xrange($start, $limit, $step = 1) {
if (0 === $step) throw new LogicException("Step can't be 0");
return (int)(abs($limit-$start) / $step) + 1;
}
Case 4 : I can count before iterating and I care about values
I can use a generator AND a count function
$args = [0,17,2];
$count = count_xrange(...$args);
$traversable = xrange(...$args);
Case 5 : Case 4, and I want all in one object
I can "decorate" an Iterator to make a Countable Iterator
function buildCountableIterator(...$args) {
$count = count_xrange(...$args);
$traversable = xrange(...$args);
return new class($count, $traversable) extends \IteratorIterator implements \Countable {
private $count;
public function __construct($count, $traversable) {
parent::__construct($traversable);
$this->count = $count;
}
public function count() {
return $this->count;
}
}
}
$countableIterator = buildCountableIterator(1, 24, 3);
// I can do this because $countableIterator is countable
$count = count($countableIterator);
// And I can do that because $countableIterator is also an Iterator
foreach($countableIterator as $item) {
// do something
}
Sources :
- http://php.net/manual/en/function.iterator-count.php
- http://php.net/manual/en/class.countable.php
- http://php.net/manual/en/class.iteratoriterator.php
- http://php.net/manual/en/language.oop5.anonymous.php
While you can't use count()
you can use a reference to set the count to make it accessible to the outside world.
function generate(&$count = 0) {
// we have 4 things
$count = 4;
for($i = 0; $i < $count; $i++) {
yield $i;
}
}
$foo = generate($count);
echo $count; // 4
foreach ($foo as $i) {
echo $i;
}
Downside to this is it won't tell you how many remain but how many it started with.
You should understand, that generator isn't data structure - it's an instance of Generator
class and, actually, it's special sort of Iterator
. Thus, you can't count its items directly (to be precise - that's because Generator
class implements only Iterator
interface, and not Countable
interface. To be honest, I can't imagine how can it implement that)
To count values with native generator you'll have to iterate through it. But that can not be done in common sense - because in most cases it's you who'll decide how many items will be yielded. Famous xrange()
sample from manual:
function xrange($start, $limit, $step = 1) {
if ($start < $limit) {
if ($step <= 0) {
throw new LogicException('Step must be +ve');
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('Step must be -ve');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
-as you can see, it's you who must define borders. And final count will depend from that. Iterating through generator will have sense only with static-borders defined generator (i.e. when count of items is always static - for example, defined inside generator strictly). In any other case you'll get parameter-dependent result. For xrange()
:
function getCount(Generator $functor)
{
$count = 0;
foreach($functor as $value)
{
$count++;
}
return $count;
}
-and usage:
var_dump(getCount(xrange(1, 100, 10)));//10
var_dump(getCount(xrange(1, 100, 1)));//100
-as you can see, "count" will change. Even worse, generator hasn't to be finite. It may yield infinite set of values (and borders are defined in external loop, for example) - and this is one more reason which makes "counting" near senseless.