Skip to content

Commit

Permalink
+ Traversal generators
Browse files Browse the repository at this point in the history
the Node class now uses the simple traversal generator instead of the iterator class
  • Loading branch information
dakujem committed Jan 29, 2024
1 parent a19808e commit 92614da
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 39 deletions.
72 changes: 50 additions & 22 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,34 +384,70 @@ Tree::link($node, $parent);
The `Tree::link` call will take care of all the relations: unlinking the original parent and linking the subtree to the new one.


## Iterators
## Tree traversal and iterators

Oliva provides iterators for tree traversal and a filter iterator.
Oliva provides tree traversal iterators, generators and a filter iterator.

The traversal iterators will iterate over **all** the tree's nodes, including the root, in a specific order.
The traversal iterators and generators will iterate over **all** the tree's nodes in a specific order.

**Depth-first search**
- `Iterator\PreOrderTraversal` pre-order traversal
- `Iterator\PostOrderTraversal` post-order traversal
**Depth-first search, pre-order traversal**
- `Traversal::preOrder` generator
- `PreOrderTraversal` iterator

**Breadth-first search**
- `Iterator\LevelOrderTraversal` level-order traversal
**Depth-first search, post-order traversal**
- `Traversal::postOrder` generator
- `PostOrderTraversal` iterator

If unsure what the above means, read more about [Tree traversal](https://en.wikipedia.org/wiki/Tree_traversal).
**Breadth-first search, level-order traversal**
- `Traversal::levelOrder` generator
- `LevelOrderTraversal` iterator

If the order of traversal, is not important, a `Node` instance can be iterated over:
>
> If unsure what the different traversals mean, read more about [Tree traversal](https://en.wikipedia.org/wiki/Tree_traversal).
>
```php
use Dakujem\Oliva\Iterator\Traversal;
use Dakujem\Oliva\Iterator\PreOrderTraversal;
use Dakujem\Oliva\Iterator\PostOrderTraversal;
use Dakujem\Oliva\Iterator\LevelOrderTraversal;

foreach(Traversal::levelOrder($root) as $node) { /* ... */ }
foreach(new LevelOrderTraversal($root) as $node) { /* ... */ }
```

> 💡
>
> The key difference between the iterator classes and generator methods is in the keys.
> If the keys in the iteration do not matter, use the `Traversal`'s generator methods for slightly better performance.
>
If the order of traversal is not important, a `Node` instance can simply be iterated over:

```php
use Dakujem\Oliva\Node;

$root = new Node( ... );

foreach ($root as $node) {
// do something useful with the nodes
// do something useful with the node
}
```

Finally, the filter iterator `Iterator\Filter` may be used for filtering either the input data or tree nodes.
The above will iterate over the whole subtree, including the node itself and all its descendants.

>
> 💡
>
> Traversals may be used to decorate nodes or even alter the trees.
> Be sure to understand how each of the traversals work before altering the tree structure within a traversal,
> otherwise you may experience unexpected.
>

### Filtering nodes

Finally, the **filter iterator** `Iterator\Filter` may be used for filtering either the input data or tree nodes.

```php
use Dakujem\Oliva\Iterator\Filter;
Expand All @@ -437,14 +473,6 @@ $node = Seed::firstOf(new Filter(
);
```

>
> 💡
>
> Traversals may be used to decorate nodes or even alter the trees.
> Be sure to understand how each of the traversals work before altering the tree structure within a traversal,
> otherwise you may experience unexpected.
>

### Searching for specific nodes

Expand Down Expand Up @@ -483,7 +511,7 @@ class TreeFinder

### Node keys

Normally, the keys will increment during a traversal (using any traversal iterator).
Normally, the keys will increment during a traversal (using any traversal iterator or generator).
```php
use Dakujem\Oliva\Node;

Expand All @@ -494,7 +522,7 @@ foreach ($root as $key => $node) {
}
```

It is possible to alter the key sequence using a key callable.
When using any of the traversal _iterators_, it is possible to alter the key sequence using a key callable.
This example generates a delimited materialized path:
```php
use Dakujem\Oliva\Iterator\PreOrderTraversal;
Expand Down
82 changes: 82 additions & 0 deletions src/Iterator/Traversal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Dakujem\Oliva\Iterator;

use Dakujem\Oliva\TreeNodeContract;
use Generator;

/**
* This class creates generators to iterate over all tree nodes in different order.
* Use these generators if you only need to iterate over the nodes without control over the keys.
*
* This implementation is more efficient than the iterator traversal implementations
* because it does not allow to modify the keys in any way.
* It is also less flexible for the same reason.
*
* @author Andrej Rypak <[email protected]>
*/
final class Traversal
{
/**
* Depth-first search pre-order traversal.
*
* Equivalent to the iterator:
* @see PreOrderTraversal
*/
public static function preOrder(TreeNodeContract $node): Generator
{
// First, yield the current node,
// then do the same for all the children.
yield $node;
foreach ($node->children() as $child) {
yield from self::preOrder($child);
}
}

/**
* Depth-first search post-order traversal.
*
* Equivalent to the iterator:
* @see PostOrderTraversal
*/
public static function postOrder(TreeNodeContract $node): Generator
{
// Yield the current node last,
// after recursively calling this for all it's children.
foreach ($node->children() as $child) {
yield from self::postOrder($child);
}
yield $node;
}

/**
* Breadth-first search (level-order) traversal.
*
* Equivalent to the iterator:
* @see LevelOrderTraversal
*/
public static function levelOrder(TreeNodeContract $node): Generator
{
// In BFS traversal a queue has to be used instead of recursion
// (recursion uses a stack - the call stack).
$queue = [$node];

// The first node in the queue is taken and yielded,
// then all of its children are added to the queue.
// This continues until there are no more nodes in the queue.
while ($node = array_shift($queue)) {
yield $node;
foreach ($node->children() as $child) {
$queue[] = $child;
}
}
}

// No not instantiate this class.
// This is enforced to avoid confusion with the traversal iterators.
private function __construct()
{
}
}
12 changes: 7 additions & 5 deletions src/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

namespace Dakujem\Oliva;

use Dakujem\Oliva\Iterator\PreOrderTraversal;
use Dakujem\Oliva\Iterator\Traversal;
use Exception;
use Generator;
use IteratorAggregate;
use JsonSerializable;

Expand Down Expand Up @@ -190,12 +191,13 @@ public function removeChildren(): self
}

/**
* Returns an iterator that iterates over this node and all its descendants in pre-order depth-first search order.
* @return PreOrderTraversal
* Returns an iterator that iterates over this node and all its descendants,
* in pre-order depth-first search order.
* @return Generator
*/
public function getIterator(): PreOrderTraversal
public function getIterator(): Generator
{
return new PreOrderTraversal($this);
return Traversal::preOrder($this);
}

/**
Expand Down
13 changes: 7 additions & 6 deletions tests/iterators.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,32 @@ require_once __DIR__ . '/setup.php';
$str .= $node->data();
}
Assert::same('FBADCEGIH', $str);
Assert::same('FBADCEGIH', TreeTesterTool::append($iterator));
Assert::same('FBADCEGIH', TreeTesterTool::chain($iterator));

$iterator = new PostOrderTraversal($root);
$str = '';
foreach ($iterator as $node) {
$str .= $node->data();
}
Assert::same('ACEDBHIGF', $str);
Assert::same('ACEDBHIGF', TreeTesterTool::append($iterator));
Assert::same('ACEDBHIGF', TreeTesterTool::chain($iterator));

$iterator = new LevelOrderTraversal($root);
$str = '';
foreach ($iterator as $i => $node) {
foreach ($iterator as $node) {
$str .= $node->data();
}
Assert::same('FBGADICEH', $str);
Assert::same('FBGADICEH', TreeTesterTool::append($iterator));
Assert::same('FBGADICEH', TreeTesterTool::chain($iterator));

Assert::type(PreOrderTraversal::class, $root->getIterator());
// Assert::type(PreOrderTraversal::class, $root->getIterator());
$str = '';
foreach ($root as $node) {
$str .= $node->data();
}
Assert::same('FBADCEGIH', $str);
Assert::same('FBADCEGIH', TreeTesterTool::append($root->getIterator()));
Assert::same('FBADCEGIH', TreeTesterTool::chain($root));
Assert::same('FBADCEGIH', TreeTesterTool::chain($root->getIterator()));
})();

(function () {
Expand Down
4 changes: 2 additions & 2 deletions tests/setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ public static function flatten(
string $traversalClass = PreOrderTraversal::class,
string $glue = '',
): string {
return self::append(
return self::chain(
new $traversalClass($node),
$glue,
);
}

public static function append(
public static function chain(
iterable $traversal,
string $glue = '',
?callable $extractor = null,
Expand Down
8 changes: 4 additions & 4 deletions tests/tool.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ require_once __DIR__ . '/setup.php';
})();

(function () {
Assert::same('', TreeTesterTool::append([]));
Assert::same('ABC', TreeTesterTool::append([
Assert::same('', TreeTesterTool::chain([]));
Assert::same('ABC', TreeTesterTool::chain([
new Node('A'),
new Node('B'),
new Node('C'),
]));
Assert::same('', TreeTesterTool::append([], '.'));
Assert::same('.A.B.C', TreeTesterTool::append([
Assert::same('', TreeTesterTool::chain([], '.'));
Assert::same('.A.B.C', TreeTesterTool::chain([
new Node('A'),
new Node('B'),
new Node('C'),
Expand Down
18 changes: 18 additions & 0 deletions tests/traversal.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Dakujem\Test;

use Dakujem\Oliva\Iterator\Traversal;
use Tester\Assert;

require_once __DIR__ . '/setup.php';

(function () {
$root = Preset::wikiTree();
Assert::same('FBADCEGIH', TreeTesterTool::chain(Traversal::preOrder($root)));
Assert::same('ACEDBHIGF', TreeTesterTool::chain(Traversal::postOrder($root)));
Assert::same('FBGADICEH', TreeTesterTool::chain(Traversal::levelOrder($root)));
})();

0 comments on commit 92614da

Please sign in to comment.