Skip to content

Commit

Permalink
tests and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
dakujem committed Jan 18, 2024
1 parent 17df57e commit 4247e80
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 45 deletions.
39 changes: 29 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Oliva

Flexible tree structure,
materialized path trees,
recursive trees and builders,
materialized path trees, recursive trees,
tree builders,
tree traversal iterators,
filter iterator.

Expand Down Expand Up @@ -169,11 +169,16 @@ $root = $builder->build(
>
> 💡
>
> Since child nodes are added to parents in the order they appear in the source data,
> Since child nodes are added to parents sequentially (i.e. without specific keys)
> in the order they appear in the source data,
> sorting the source collection by path _prior_ to building the tree may be a good idea.
>
> If sorting of siblings is needed _after_ a tree has been built,
> one of the provided iterators can be used to traverse and modify the tree.
> `LevelOrderTraversal` can be used to traverse and modify the tree.
>
> The same is true for cases where the children need to be keyed by specific keys.
> Use `LevelOrderTraversal`, remove the children, then sort them and/or calculate their keys
> and add them back under the new keys and/or in the new order.
>

Expand Down Expand Up @@ -480,13 +485,8 @@ All Oliva traversal iterators accept a key callable and a starting vector (a pre
>

## Caveats
## Cookbook

>
> This section is very relevant if migrating from the previous library ([`oliva/tree`](https://github.com/dakujem/oliva-tree)).
>
> It may be useful for others too, it provides solutions to real-world scenarios.
>

### Materialized path tree without root data

Expand Down Expand Up @@ -576,6 +576,25 @@ $root = $builder->build(
If a node's parent matches the value, it is considered the root node.


## Migrating from the old oliva/tree library

**Builders and iterators**

If migrating from the previous library ([`oliva/tree`](https://github.com/dakujem/oliva-tree)), the most common problems are caused by
- the builders not automatically adding an empty root node
- the iterators iterating over root node

For both, see "Materialized path tree without root data" and "Recursive tree without root data" sections above.

**Node classes**

Neither magic props proxying nor array access of the `Oliva\Utils\Tree\Node\Node` are supported.
Migrating to the new `Dakujem\Oliva\Node` class is recommended instead of attempting to recreate the old behaviour.

The `Dakujem\Oliva\Node` is very similar to `Oliva\Utils\Tree\Node\SimpleNode`, however,
migrating from that one should be trivial (migrate the getter/setter usage).


## Testing

Run unit tests using the following command:
Expand Down
19 changes: 19 additions & 0 deletions src/MaterializedPath/InvalidTreePath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Dakujem\Oliva\MaterializedPath;

use RuntimeException;
use Throwable;

/**
* @author Andrej Rypak <[email protected]>
*/
final class InvalidTreePath extends RuntimeException
{
public function __construct($message = null, $code = null, Throwable $previous = null)
{
parent::__construct($message ?? 'The given value is not a valid tree path.', $code ?? 0, $previous);
}
}
4 changes: 2 additions & 2 deletions src/MaterializedPath/Support/AlmostThere.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class AlmostThere
{
public function __construct(
private ?TreeNodeContract $root,
private ShadowNode $shadowRoot,
private ?ShadowNode $shadowRoot,
) {
}

Expand All @@ -36,7 +36,7 @@ public function root(): ?TreeNodeContract
* because there may be nodes that are not connected to the root due to inconsistent input data.
* These nodes are present and reachable within the shadow tree.
*/
public function shadow(): ShadowNode
public function shadow(): ?ShadowNode
{
return $this->shadowRoot;
}
Expand Down
14 changes: 4 additions & 10 deletions src/MaterializedPath/TreeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static function fixed(int $levelWidth, callable $accessor): callable
}
if (!is_string($path)) {
// TODO improve exceptions (index/path etc)
throw new LogicException('Invalid path returned.');
throw new InvalidTreePath('Invalid tree path returned by the accessor. A string is required.');
}
return str_split($path, $levelWidth);
};
Expand All @@ -94,7 +94,7 @@ public static function delimited(string $delimiter, callable $accessor): callabl
}
if (!is_string($path)) {
// TODO improve exceptions (index/path etc)
throw new LogicException('Invalid path returned.');
throw new InvalidTreePath('Invalid tree path returned by the accessor. A string is required.');
}
$path = trim($path, $delimiter);
if ('' === $path) {
Expand Down Expand Up @@ -123,7 +123,7 @@ public function processInput(iterable $input): AlmostThere

// The actual tree nodes are not yet connected.
// Reconstruct the tree using the shadow tree's structure.
$root = $shadowRoot->reconstructRealTree();
$root = $shadowRoot?->reconstructRealTree();

// For edge case handling, return a structure containing the shadow root as well as the actual tree root.
return new AlmostThere(
Expand All @@ -136,18 +136,12 @@ private function buildShadowTree(
iterable $input,
callable $nodeFactory,
callable $vectorExtractor,
): ShadowNode {
): ?ShadowNode {
$register = new Register();
foreach ($input as $inputIndex => $data) {
// Create a node using the provided factory.
$node = $nodeFactory($data, $inputIndex);

// Enable skipping particular data.
// TODO use input filter instead
// if (null === $node) {
// continue;
// }

// Check for consistency.
if (!$node instanceof MovableNodeContract) {
// TODO improve exceptions
Expand Down
12 changes: 12 additions & 0 deletions tests/iterators.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use Dakujem\Oliva\DataNodeContract;
use Dakujem\Oliva\Iterator\LevelOrderTraversal;
use Dakujem\Oliva\Iterator\PostOrderTraversal;
use Dakujem\Oliva\Iterator\PreOrderTraversal;
use Dakujem\Oliva\Iterator\Support\Counter;
use Dakujem\Oliva\Node;
use Dakujem\Oliva\TreeNodeContract;
use Tester\Assert;
Expand Down Expand Up @@ -217,4 +218,15 @@ $expected = [
Assert::same($expected, array_map(fn(DataNodeContract $node) => $node->data(), iterator_to_array($iterator)));


$counter = new Counter();
Assert::same(0, $counter->current());
Assert::same(0, $counter->touch());
Assert::same(1, $counter->touch());
Assert::same(2, $counter->current());
Assert::same(3, $counter->next());
Assert::same(3, $counter->current());

$counter = new Counter(5);
Assert::same(5, $counter->current());

//$root->addChild(new)
115 changes: 92 additions & 23 deletions tests/mptree.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -46,39 +46,108 @@ $tree = $builder->processInput(
input: Seed::nullFirst($data),
);

$it = new PreOrderTraversal($tree->root(), fn(
TreeNodeContract $node,
array $vector,
int $seq,
int $counter,
): string => '>' . implode('.', $vector));
foreach ($it as $key => $node) {
$item = $node->data();
if (null === $item) {
echo '>root' . "\n";
continue;
}
$pad = str_pad($key, 10, ' ', STR_PAD_LEFT);
echo "$pad {$item->id} {$item->path}\n";
}

//xdebug_break();

new Filter($it, Seed::omitNull());
new Filter($it, Seed::omitRoot());

$item = $tree->root()?->data();

Assert::type(AlmostThere::class, $tree);
Assert::type(Node::class, $tree->root());
Assert::null($tree->root()?->data());
Assert::type(Item::class, Seed::first($tree->root()?->children())?->data());

// rekalkulacia / presuny ?


// propagacia zmeny (hore/dole) (eventy?)


$vectorExtractor = TreeBuilder::fixed(
3,
fn(mixed $path) => $path,
);
Assert::same(['000', '000'], $vectorExtractor('000000'));
Assert::same(['foo', 'bar'], $vectorExtractor('foobar'));
Assert::same([], $vectorExtractor(''));
Assert::same([], $vectorExtractor(null));
Assert::throws(function () use ($vectorExtractor) {
$vectorExtractor(4.2);
}, \RuntimeException::class); // TODO improve


// an empty input can not result in any tree
Assert::throws(function () use ($builder) {
$builder->build([]);
}, RuntimeException::class, 'Corrupted input, no tree created.'); // TODO improve


$failingBuilder = new TreeBuilder(fn() => null, fn() => []);
Assert::throws(function () use ($failingBuilder) {
$failingBuilder->build([null]);
}, LogicException::class, 'The node factory must return a movable node instance.'); // TODO improve

$invalidVector = new TreeBuilder(fn() => new Node(null), fn() => null);
Assert::throws(function () use ($invalidVector) {
$invalidVector->build([null]);
}, LogicException::class, 'The vector calculator must return an array.'); // TODO improve


$invalidVectorContents = new TreeBuilder(fn() => new Node(null), fn() => ['a', null]);
Assert::throws(function () use ($invalidVectorContents) {
$invalidVectorContents->build([null]);
}, LogicException::class, 'The vector may only consist of strings or integers.'); // TODO improve


$duplicateVector = new TreeBuilder(fn() => new Node(null), fn() => ['any']);
Assert::throws(function () use ($duplicateVector) {
$duplicateVector->build([null, null]);
}, LogicException::class, 'Duplicate node vector: any'); // TODO improve



$collection = [
new Item(id: 0, path: ''), // the root
new Item(id: 1, path: '.0'),
new Item(id: 2, path: '.1'),
new Item(id: 3, path: '.3'),
new Item(id: 4, path: '.0.0'),
new Item(id: 5, path: '.2.0'),
new Item(id: 6, path: '.2'),
new Item(id: 7, path: '.0.1'),
];

$builder = new TreeBuilder(
node: fn(Item $item) => new Node($item),
vector: TreeBuilder::delimited(
delimiter: '.',
accessor: fn(Item $item) => $item->path,
),
);

$root = $builder->build(
input: $collection,
);

$iterator = function(TreeNodeContract $root) {
$it = new PreOrderTraversal($root, fn(
TreeNodeContract $node,
array $vector,
int $seq,
int $counter,
): string => '>' . implode('.', $vector));
return array_map(function (Node $item): string {
$data = $item->data();
return "[$data->id]";
}, iterator_to_array($it));
};


//new Filter($it, Seed::omitNull());
//new Filter($it, Seed::omitRoot());

Assert::same([
'>' => '[0]',
'>0' => '[1]',
'>0.0' => '[4]',
'>0.1' => '[7]',
'>1' => '[2]',
'>2' => '[3]',
'>3' => '[6]',
'>3.0' => '[5]',
], $iterator($root));

0 comments on commit 4247e80

Please sign in to comment.