diff --git a/readme.md b/readme.md index 6548319..b50c0d9 100644 --- a/readme.md +++ b/readme.md @@ -384,22 +384,45 @@ 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; @@ -407,11 +430,24 @@ 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; @@ -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 @@ -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; @@ -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; diff --git a/src/Iterator/Traversal.php b/src/Iterator/Traversal.php new file mode 100644 index 0000000..429f1b0 --- /dev/null +++ b/src/Iterator/Traversal.php @@ -0,0 +1,82 @@ + + */ +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() + { + } +} diff --git a/src/Node.php b/src/Node.php index f6029e1..f7d1065 100644 --- a/src/Node.php +++ b/src/Node.php @@ -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; @@ -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); } /** diff --git a/tests/iterators.phpt b/tests/iterators.phpt index 0cd1af9..8a7e9cc 100644 --- a/tests/iterators.phpt +++ b/tests/iterators.phpt @@ -40,7 +40,7 @@ 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 = ''; @@ -48,23 +48,24 @@ require_once __DIR__ . '/setup.php'; $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 () { diff --git a/tests/setup.php b/tests/setup.php index 5c2471b..fabbb02 100644 --- a/tests/setup.php +++ b/tests/setup.php @@ -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, diff --git a/tests/tool.phpt b/tests/tool.phpt index 1114d94..590563c 100644 --- a/tests/tool.phpt +++ b/tests/tool.phpt @@ -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'), diff --git a/tests/traversal.phpt b/tests/traversal.phpt new file mode 100644 index 0000000..a2e0ba5 --- /dev/null +++ b/tests/traversal.phpt @@ -0,0 +1,18 @@ +