Skip to content

Commit

Permalink
recursive builder
Browse files Browse the repository at this point in the history
  • Loading branch information
dakujem committed Jan 14, 2024
1 parent 7f31d1f commit 953632d
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 21 deletions.
7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ $root = $builder->build(

Since child nodes are be added to parents 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.


## Builders

Caveats:
- no root in data
- null data rows

6 changes: 3 additions & 3 deletions src/MaterializedPath/Support/ShadowNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
use LogicException;

/**
* ShadowNode
* Shadow node used internally when building materialized path trees.
*
* @author Andrej Rypak <[email protected]>
*/
final class ShadowNode extends Node implements MovableNodeContract
{
public function __construct(
?TreeNodeContract $node,
?MovableNodeContract $node,
?ShadowNode $parent = null,
) {
parent::__construct(
Expand Down Expand Up @@ -50,7 +50,7 @@ public function reconstructRealTree(): ?TreeNodeContract
return $realNode;
}

public function realNode(): ?TreeNodeContract
public function realNode(): ?MovableNodeContract
{
return $this->data();
}
Expand Down
28 changes: 16 additions & 12 deletions src/MaterializedPath/TreeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,37 @@
use Dakujem\Oliva\MaterializedPath\Support\Register;
use Dakujem\Oliva\MaterializedPath\Support\ShadowNode;
use Dakujem\Oliva\MaterializedPath\Support\Tree;
use Dakujem\Oliva\MovableNodeContract;
use Dakujem\Oliva\TreeNodeContract;
use LogicException;
use RuntimeException;

/**
* Materialized path tree builder. Builds trees from flat data collections.
* Materialized path tree builder.
* Builds trees from flat data collections.
* Each item of a collection must contain path information.
*
* The builder needs to be provided an iterable data collection, a node factory
* and a vector extractor that returns the node's vector based on the data.
* The extractor will typically be a simple function that takes a path prop/attribute from the data item
* and a vector extractor that returns the node's path vector based on the data.
* The extractor will typically be a simple function that takes a serialized path prop/attribute from the data item
* and splits or explodes it into a vector.
* Two common-case extractors can be created using the `fixed` and `delimited` methods.
*
* Fixed path variant example:
* ```
* $root = (new MaterializedPathTreeBuilder())->build(
* $root = (new TreeBuilder())->build(
* $myItemCollection,
* fn(MyItem $item) => new Node($item),
* MaterializedPathTreeBuilder::fixed(3, fn(MyItem $item) => $item->path),
* TreeBuilder::fixed(3, fn(MyItem $item) => $item->path),
* );
* ```
*
* Delimited path variant example:
* ```
* $root = (new MaterializedPathTreeBuilder())->build(
* $root = (new TreeBuilder())->build(
* $myItemCollection,
* fn(MyItem $item) => new Node($item),
* MaterializedPathTreeBuilder::delimited('.', fn(MyItem $item) => $item->path),
* TreeBuilder::delimited('.', fn(MyItem $item) => $item->path),
* );
* ```
*
Expand Down Expand Up @@ -117,14 +120,15 @@ private function buildShadowTree(
$node = $nodeFactory($data, $inputIndex);

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

// Check for consistency.
if (!$node instanceof TreeNodeContract) {
if (!$node instanceof MovableNodeContract) {
// TODO improve exceptions
throw new LogicException('The node factory must return a node instance.');
throw new LogicException('The node factory must return a movable node instance.');
}

// Calculate the node's vector.
Expand Down
129 changes: 129 additions & 0 deletions src/Recursive/TreeBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types=1);

namespace Dakujem\Oliva\Recursive;

use Dakujem\Oliva\MovableNodeContract;
use Dakujem\Oliva\TreeNodeContract;
use LogicException;

/**
* Recursive tree builder.
* Builds trees from flat data collections.
* Each item of a collection must contain self reference (an ID) and a reference to its parent.
*
* Example for collections containing items with `id` and `parent` props, the root being the node with `null` parent:
* ```
* $root = (new TreeBuilder())->build(
* $myItemCollection,
* fn(MyItem $item) => new Node($item),
* TreeBuilder::prop('id'),
* TreeBuilder::prop('parent'),
* );
* ```
*
* @author Andrej Rypak <[email protected]>
*/
final class TreeBuilder
{
public static function prop(string $name): callable
{
return fn(object $item) => $item->{$name} ?? null;
}

public static function attr(string $name): callable
{
return fn(array $item) => $item[$name] ?? null;
}

public function build(
iterable $input,
callable $node,
callable $self,
callable $parent,
string|int|null $root = null,
): TreeNodeContract {
[$root] = $this->processData(
input: $input,
nodeFactory: $node,
selfRefExtractor: $self,
parentRefExtractor: $parent,
rootRef: $root,
);
return $root;
}

private function processData(
iterable $input,
callable $nodeFactory,
callable $selfRefExtractor,
callable $parentRefExtractor,
string|int|null $rootRef = null,
): array {
/** @var array<string|int, array<int, string|int>> $childRegister */
$childRegister = [];
/** @var array<string|int, MovableNodeContract> $nodeRegister */
$nodeRegister = [];
// $root = null;
foreach ($input as $inputIndex => $data) {
// Create a node using the provided factory.
$node = $nodeFactory($data, $inputIndex);

// Check for consistency.
if (!$node instanceof MovableNodeContract) {
// TODO improve exceptions
throw new LogicException('The node factory must return a movable node instance.');
}

$self = $selfRefExtractor($data, $inputIndex, $node);
$parent = $parentRefExtractor($data, $inputIndex, $node);

if (isset($nodeRegister[$self])) {
// TODO improve exceptions
throw new LogicException('Duplicate node reference: ' . $self);
}
$nodeRegister[$self] = $node;

// No parent, when this node is the root.
if ($rootRef === $self) {
continue;
}

if (!isset($childRegister[$parent])) {
$childRegister[$parent] = [];
}
$childRegister[$parent][] = $self;
}
$this->connectNode(
$nodeRegister,
$childRegister,
$rootRef,
);

return [
$nodeRegister[$rootRef],
$nodeRegister,
$childRegister,
];
}

/**
* @param array<string|int, MovableNodeContract> $nodeRegister
* @param array<string|int, array<int, string|int>> $childRegister
*/
private function connectNode(array $nodeRegister, array $childRegister, string|int|null $ref): void
{
$parent = $nodeRegister[$ref];
foreach ($childRegister[$ref] ?? [] as $childRef) {
$child = $nodeRegister[$childRef];
$child->setParent($parent);
$parent->addChild($child);
$this->connectNode(
$nodeRegister,
$childRegister,
$childRef,
);
}
}
}
16 changes: 10 additions & 6 deletions tests/mptree.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

declare(strict_types=1);

use Dakujem\Oliva\DataNodeContract;
use Dakujem\Oliva\Iterator\Data;
use Dakujem\Oliva\Iterator\Filter;
use Dakujem\Oliva\Iterator\PreOrderTraversal;
Expand Down Expand Up @@ -44,14 +43,19 @@ $tree = $builder->buildTree(
),
);

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

Expand Down
43 changes: 43 additions & 0 deletions tests/recursive.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

use Dakujem\Oliva\Iterator\Data;
use Dakujem\Oliva\Node;
use Dakujem\Oliva\Recursive\TreeBuilder;
use Tester\Environment;

require_once __DIR__ . '/../vendor/autoload.php';
Environment::setup();

class Item
{
public function __construct(
public int $id,
public ?int $parent,
) {
}
}

$data = [
new Item(1, 2),
new Item(2, 4),
new Item(3, 4),
new Item(4, null),
new Item(5, 4),
new Item(77, 42),
new Item(8, 7),
new Item(6, 5),
];

$builder = new TreeBuilder();

$tree = $builder->build(
input: Data::nullFirst($data),
node: fn(?Item $item) => new Node($item),
self: fn(?Item $item) => $item?->id,
parent: fn(?Item $item) => $item?->parent,
);

xdebug_break();
$foo = 1;

0 comments on commit 953632d

Please sign in to comment.