diff --git a/src/MaterializedPath/FixedPathCalculator.php b/src/MaterializedPath/FixedPathCalculator.php new file mode 100644 index 0000000..932a872 --- /dev/null +++ b/src/MaterializedPath/FixedPathCalculator.php @@ -0,0 +1,146 @@ + + */ +final class FixedPathCalculator +{ + /** + * Number of characters to store a single position within a path, + * with one position for each level of depth. + * + * If this value is changed, the tree paths must be recalculated. + */ + private int $charsPerPosition; + + private bool $base36; + + public function __construct( + int $charsPerPosition, + bool $base36 = true, + ) { + $this->charsPerPosition = $charsPerPosition; + $this->base36 = $base36; + } + + /** + * Recalculate a path string into a numeric vector. + * A vector is a sequence of indexes from the tree's root to a node. + * ``` + * "" --> [] + * "000" --> [0] + * "00c001" --> [12, 1] + * ``` + */ + public function pathToVector(string $path): array + { + if ('' === $path) { + return []; + } + return array_map( + fn(string $v) => self::posToNum($v), + str_split($path, $this->charsPerPosition), + ); + } + + /** + * Recalculate a numeric vector into a path string. + * ``` + * [] --> "" + * [0] --> "000" + * [12, 1] --> "00c001" + * ``` + */ + public function vectorToPath(array $vector): string + { + return implode( + '', + array_map(fn(int $v) => self::numToPos($v), $vector), + ); + } + + /** + * Convert a string of a single position into a vector element. + * ``` + * 000 --> 0 + * 001 --> 1 + * 00c --> 12 + * ``` + */ + public function posToNum(string $position): int + { + if (!$this->base36) { + return (int)$position; + } + return (int)base_convert($position, 36, 10); + } + + /** + * Convert a numeric vector element to a position string. + * ``` + * 0 --> 000 + * 1 --> 001 + * 12 --> 00c + * ``` + */ + public function numToPos(int $element): string + { + if (!$this->base36) { + return (string)$element; + } + $x = base_convert((string)$element, 10, 36); + return str_pad($x, $this->charsPerPosition, '0', STR_PAD_LEFT); + } + + /** + * Calculate the depth of a node with the given path. + */ + public function pathToDepth(string $path): int + { + $depth = strlen($path) / $this->charsPerPosition; + if (!is_int($depth)) { + throw new InvalidTreePath('The given tree path "' . $path . '" is invalid. The path`s length is expected to be a multiple of ' . $this->charsPerPosition . '.'); + } + return $depth; + } + + /** + * Calculate the path length for a given depth. + * + * Note: This method has no equivalent for the delimited MPT variant where the path length is not fixed. + */ + public function depthToPathLength(int $depth): int + { + return $depth * $this->charsPerPosition; + } +} diff --git a/src/MaterializedPath/InvalidTreePath.php b/src/MaterializedPath/InvalidTreePath.php new file mode 100644 index 0000000..3858640 --- /dev/null +++ b/src/MaterializedPath/InvalidTreePath.php @@ -0,0 +1,19 @@ + + */ +final class InvalidTreePath extends RuntimeException +{ + public function __construct($message = null, $code = null, Throwable $previous = null) + { + parent::__construct($message ?? 'The given string is not a valid tree path.', $code, $previous); + } +}