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.