Skip to content

Commit

Permalink
mp tree
Browse files Browse the repository at this point in the history
  • Loading branch information
dakujem committed Jan 12, 2024
1 parent 88a58dc commit 05ad3aa
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 45 deletions.
35 changes: 35 additions & 0 deletions src/MaterializedPath/Support/Register.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Dakujem\Oliva\MaterializedPath\Support;

/**
* @internal
*
* @author Andrej Rypak <[email protected]>
*/
final class Register
{
private array $nodes = [];

public function contains(array $vector): bool
{
return isset($this->nodes[$this->makeIndex($vector)]);
}

public function push(array $vector, ShadowNode $node): void
{
$this->nodes[$this->makeIndex($vector)] = $node;
}

public function pull(array $vector): ?ShadowNode
{
return $this->nodes[$this->makeIndex($vector)] ?? null;
}

private function makeIndex(array $vector): string
{
return count($vector) . chr(22) . implode(chr(7), $vector);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Dakujem\Oliva\Builder;
namespace Dakujem\Oliva\MaterializedPath\Support;

use Dakujem\Oliva\MovableNodeContract;
use Dakujem\Oliva\Node;
Expand All @@ -17,17 +17,24 @@
final class ShadowNode extends Node implements MovableNodeContract
{
public function __construct(
?TreeNodeContract $realNode,
ShadowNode $parent,
string $path, // alebo vector?
string|int $key,
?TreeNodeContract $node,
?ShadowNode $parent = null,
) {
parent::__construct(
data: $realNode,
data: $node,
parent: $parent,
);
}

/**
* Reconstruct the real tree according to the connections of the shadow tree.
* Reflect all the shadow tree's child-parent links to the actual tree
* and return the root.
*
* Note:
* Should only be called on a root shadow node,
* otherwise a non-root node may be returned.
*/
public function reconstructRealTree(): ?TreeNodeContract
{
$realNode = $this->realNode();
Expand All @@ -43,7 +50,7 @@ public function reconstructRealTree(): ?TreeNodeContract
return $realNode;
}

private function realNode(): ?TreeNodeContract
public function realNode(): ?TreeNodeContract
{
return $this->data();
}
Expand Down
43 changes: 43 additions & 0 deletions src/MaterializedPath/Support/Tree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Dakujem\Oliva\MaterializedPath\Support;

use Dakujem\Oliva\TreeNodeContract;

/**
* A tree built from flat data.
*
* This structure allows for data inspection, data correction or debugging
* and is not directly intended to be used in applications.
*
* @author Andrej Rypak <[email protected]>
*/
class Tree
{
public function __construct(
private TreeNodeContract $root,
private ShadowNode $shadowRoot,
) {
}

/**
* Return the actual tree root.
*/
public function root(): TreeNodeContract
{
return $this->root;
}

/**
* Return the shadow tree root.
* This shadow tree may be used for edge case handling, data reconstruction, inspections and debugging,
* 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
{
return $this->shadowRoot;
}
}
211 changes: 211 additions & 0 deletions src/MaterializedPath/TreeBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php

declare(strict_types=1);

namespace Dakujem\Oliva\MaterializedPath;

use Dakujem\Oliva\MaterializedPath\Support\Register;
use Dakujem\Oliva\MaterializedPath\Support\ShadowNode;
use Dakujem\Oliva\MaterializedPath\Support\Tree;
use Dakujem\Oliva\TreeNodeContract;
use LogicException;

/**
* Materialized path tree builder. Builds trees from flat data collections.
*
* 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 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(
* $myItemCollection,
* fn(MyItem $item) => new Node($item),
* MaterializedPathTreeBuilder::fixed(3, fn(MyItem $item) => $item->path),
* );
* ```
*
* Delimited path variant example:
* ```
* $root = (new MaterializedPathTreeBuilder())->build(
* $myItemCollection,
* fn(MyItem $item) => new Node($item),
* MaterializedPathTreeBuilder::delimited('.', fn(MyItem $item) => $item->path),
* );
* ```
*
* @author Andrej Rypak <[email protected]>
*/
final class TreeBuilder
{
public static function fixed(int $levelWidth, callable $accessor): callable
{
return function (mixed $data) use ($levelWidth, $accessor) {
$path = $accessor($data);
if (null === $path) {
return [];
}
if (!is_string($path)) {
// TODO improve exceptions (index/path etc)
throw new LogicException('Invalid path returned.');
}
return str_split($path, $levelWidth);
};
}

public static function delimited(string $delimiter, callable $accessor): callable
{
return function (mixed $data) use ($delimiter, $accessor) {
$path = $accessor($data);
if (null === $path) {
return [];
}
if (!is_string($path)) {
// TODO improve exceptions (index/path etc)
throw new LogicException('Invalid path returned.');
}
return explode($delimiter, $path);
};
}

public function build(
iterable $input,
callable $node,
callable $vector,
): TreeNodeContract {
return $this->buildTree($input, $node, $vector)->root();
}

public function buildTree(
iterable $input,
callable $node,
callable $vector,
): Tree {
$shadowRoot = $this->buildShadowTree(
$input,
$node,
$vector,
);

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

// For edge case handling, return a structure containing the shadow root as well as the actual tree root.
return new Tree(
root: $root,
shadowRoot: $shadowRoot,
);
}

private function buildShadowTree(
iterable $input,
callable $nodeFactory,
callable $vectorExtractor,
): ShadowNode {
$register = new Register();
foreach ($input as $inputIndex => $data) {
// Create a node using the provided factory.
$node = $nodeFactory($data, $inputIndex);

// Enable skipping particular data.
if (null === $node) {
continue;
}

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

// Calculate the node's vector.
$vector = $vectorExtractor($data, $inputIndex, $node);
if (!is_array($vector)) {
// TODO improve exceptions
throw new LogicException('The vector calculator must return an array.');
}
foreach ($vector as $i) {
if (!is_string($i) && !is_integer($i)) {
// TODO improve exceptions
throw new LogicException('The vector may only consist of strings or integers.');
}
}

// Check for node collisions.
$existingNode = $register->pull($vector);
if ($existingNode->realNode() instanceof TreeNodeContract) {
// TODO improve exceptions
throw new LogicException('Duplicate node vector: ' . implode('.', $vector));
}

$shadow = new ShadowNode($node);

// Finally, connect the node to the tree.
// Make sure all the (shadow) nodes exist all the way to the root.
$this->connectNode($shadow, $vector, $register/*, $key*/);
}

// Pull the shadow root from the register.
return $register->pull([]);
}

/**
* Recursion.
*/
private function connectNode(ShadowNode $node, array $vector, Register $register): void
{
$existingNode = $register->pull($vector);

// If the node is already in the registry, replace the real node and return.
if (null !== $existingNode) {
// We first need to check for node collisions.
if (null !== $node->realNode() && null !== $existingNode->realNode()) {
// TODO improve exceptions
throw new LogicException('Duplicate node vector: ' . implode('.', $vector));
}
$existingNode->fill($node->realNode());
return;
}

// Register the node.
$register->push($vector, $node);

// Recursively connect ancestry all the way up to the root.
$this->connectAncestry($node, $vector, $register);
}

private function connectAncestry(ShadowNode $node, array $vector, Register $register): void
{
// When the node is a root itself, abort recursion.
if (count($vector) === 0) {
return;
}

// Attempt to pull the parent node from the registry.
array_pop($vector);
$parent = $register->pull($vector);

// If the parent is already in the registry, only bind the node to the parent and abort.
if (null !== $parent) {
// Establish parent-child relationship.
$node->setParent($parent);
$parent->addChild($node);
}

// Otherwise create a bridging node, push it to the registry, link them and continue recursively,
// hoping other iterations will fill the parent.
$parent = new ShadowNode(null);
$register->push($vector, $parent);

// Establish parent-child relationship.
$node->setParent($parent);
$parent->addChild($node);

// Continue with the next ancestor.
$this->connectAncestry($parent, $vector, $register);
}
}
36 changes: 0 additions & 36 deletions src/draft.php

This file was deleted.

2 changes: 0 additions & 2 deletions tests/draft.phpt → tests/iterators.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ use Tester\Assert;
use Tester\Environment;

require_once __DIR__ . '/../vendor/autoload.php';

// tester
Environment::setup();

$a = new Node('A');
Expand Down
Loading

0 comments on commit 05ad3aa

Please sign in to comment.