Skip to content

Commit

Permalink
Combinator
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Jan 17, 2024
1 parent 30f33ad commit 3b7ec1b
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 0 deletions.
95 changes: 95 additions & 0 deletions src/Iterators/CachedIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Iterators;


class CachedIterator implements \Iterator, \Countable, \SeekableIterator
{
private \Iterator $iterator;
private int $offset = -1;
private array $keys = [];
private array $values = [];


public function __construct(iterable $iterator)
{
$this->iterator = is_array($iterator) ? new \ArrayIterator($iterator) : $iterator;

Check failure on line 23 in src/Iterators/CachedIterator.php

View workflow job for this annotation

GitHub Actions / PHPStan

Property Nette\Iterators\CachedIterator::$iterator (Iterator) does not accept Traversable<mixed, mixed>.
}


public function rewind(): void
{
$this->offset = 0;
$this->fetchUntil(0);
}


public function current(): mixed
{
return $this->values[$this->offset];
}


public function key(): mixed
{
return $this->keys[$this->offset];
}


public function next(): void
{
if ($this->offset < count($this->values)) {
$this->offset++;
$this->fetchUntil($this->offset);
}
}


public function valid(): bool
{
return $this->offset < count($this->values);
}


public function count(): int
{
$this->fetchUntil(PHP_INT_MAX);
return count($this->values);
}


public function seek(int $offset): void
{
if ($offset >= count($this->values)) {
$this->fetchUntil($offset);
}
if ($offset < 0 || $offset >= count($this->values)) {
throw new \OutOfBoundsException("Invalid seek position $offset");
}
$this->offset = $offset;
}


private function fetchUntil(int $limit): void
{
while ($limit >= count($this->values)) {
if ($this->values) {
$this->iterator->next();
} else {
$this->iterator->rewind();
}
if (!$this->iterator->valid()) {
break;
}
$this->keys[] = $this->iterator->key();
$this->values[] = $this->iterator->current();
}
}
}
68 changes: 68 additions & 0 deletions src/Iterators/Combinator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Iterators;


class Combinator implements \Iterator, \Countable, \SeekableIterator
{
private int $offset = 0;


public function __construct(
private array $keys,
private array $values,
) {
}


public function rewind(): void
{
$this->offset = 0;
}


public function current(): mixed
{
return $this->values[$this->offset];
}


public function key(): mixed
{
return $this->keys[$this->offset];
}


public function next(): void
{
$this->offset++;
}


public function valid(): bool
{
return $this->offset < count($this->values);
}


public function count(): int
{
return count($this->values);
}


public function seek(int $offset): void
{
if ($offset < 0 || $offset >= count($this->values)) {
throw new \OutOfBoundsException("Invalid seek position $offset");
}
$this->offset = $offset;
}
}
60 changes: 60 additions & 0 deletions tests/Iterators/CachedIterator.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* Test: Nette\Iterators\CachedIterator
*/

declare(strict_types=1);

use Nette\Iterators\CachedIterator;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


$data = ['a' => 'apple', 'b' => 'banana', 'c' => 'cherry'];

// iteration
$iterator = new CachedIterator($data);
$expectedKeys = array_keys($data);
$expectedValues = array_values($data);
$index = 0;
foreach ($iterator as $key => $value) {
Assert::same($expectedKeys[$index], $key);
Assert::same($expectedValues[$index], $value);
$index++;
}
Assert::same(count($data), $index);

// re-iteration
$index = 0;
foreach ($iterator as $key => $value) {
Assert::same($expectedKeys[$index], $key);
Assert::same($expectedValues[$index], $value);
$index++;
}
Assert::same(count($data), $index);

// count + re-iteration
$iterator = new CachedIterator($data);
Assert::same(count($data), count($iterator));
$index = 0;
foreach ($iterator as $key => $value) {
Assert::same($expectedKeys[$index], $key);
Assert::same($expectedValues[$index], $value);
$index++;
}
Assert::same(count($data), $index);

// seek
$iterator = new CachedIterator($data);
$iterator->seek(1);
Assert::same('banana', $iterator->current());
Assert::exception(
fn() => $iterator->seek(-1),
OutOfBoundsException::class,
);
Assert::exception(
fn() => $iterator->seek(3),
OutOfBoundsException::class,
);
51 changes: 51 additions & 0 deletions tests/Iterators/Combinator.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/**
* Test: Nette\Iterators\Combinator
*/

declare(strict_types=1);

use Nette\Iterators\Combinator;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


$keys = [true, false];
$values = ['value1', 'value2'];
$combinator = new Combinator($keys, $values);
Assert::same(count($combinator), count($values));

// iteration
$index = 0;
foreach ($combinator as $key => $value) {
Assert::equal($keys[$index], $key);
Assert::equal($values[$index], $value);
$index++;
}

// count
Assert::same(count($values), $index);

// re-iteration
$index = 0;
foreach ($combinator as $key => $value) {
Assert::equal($keys[$index], $key);
Assert::equal($values[$index], $value);
$index++;
}

Assert::same(count($values), $index);

// seek
$combinator->seek(1);
Assert::same('value2', $combinator->current());
Assert::exception(
fn() => $combinator->seek(-1),
OutOfBoundsException::class,
);
Assert::exception(
fn() => $combinator->seek(2),
OutOfBoundsException::class,
);

0 comments on commit 3b7ec1b

Please sign in to comment.