Home » Php » PHP – How to count a generators yields

PHP – How to count a generators yields

Posted by: admin July 12, 2020 Leave a comment

Questions:

Using PHP >= 5.5 if we have a method that yielded values, what would be the best method in counting these values?

What I was expecting was to be able to convert a Generator to an array and count that, however it would return an empty array. Count() also does not work as the Generator is reported as empty despite not being empty.

I’m baffled with this. If you don’t need to count a generators yields then it’s a nice feature otherwise I don’t see much of a point for it. There is a way to detect if a generator is empty, this is by using the key() method and if it returns NULL then there are either no yields or the generator has already been iterated through which would mean the current pointer is null.

How to&Answers:

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.

Answer:

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().

Answer:

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 :

Answer:

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.