Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
SearchAbstract
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
14 / 14
17
100.00% covered (success)
100.00%
1 / 1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 current
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCurrent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStartNode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setStartNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 next
n/a
0 / 0
n/a
0 / 0
0
 getNodes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 atMaxLevels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentLevel
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getMaxLevels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxLevels
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 invalidate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * @author: Doug Wilbourne (dougwilbourne@gmail.com)
5 */
6
7declare(strict_types=1);
8
9namespace pvc\struct\treesearch;
10
11use pvc\interfaces\struct\treesearch\NodeSearchableInterface;
12use pvc\interfaces\struct\treesearch\SearchInterface;
13use pvc\struct\treesearch\err\SetMaxSearchLevelsException;
14use pvc\struct\treesearch\err\StartNodeUnsetException;
15
16/**
17 * Class SearchAbstract
18 * @template NodeType of NodeSearchableInterface
19 *
20 * @implements SearchInterface<NodeType>
21 */
22abstract class SearchAbstract implements SearchInterface
23{
24    /**
25     * @var NodeType
26     */
27    protected mixed $startNode;
28
29    /**
30     * @var NodeType|null
31     */
32    protected mixed $currentNode = null;
33
34    /**
35     * @var int
36     *
37     * maximum depth/height to which we are allowed to traverse the tree.
38     * Searching children goes down the tree, searching ancestors goes up the
39     * tree
40     */
41    private int $maxLevels = PHP_INT_MAX;
42
43    /**
44     * @var non-negative-int int
45     */
46    private int $currentLevel = 0;
47
48    /**
49     * key
50     *
51     * @return non-negative-int|null
52     */
53    public function key(): int|null
54    {
55        return $this->current()?->getNodeId();
56    }
57
58    /**
59     * current
60     *
61     * @return NodeType|null
62     */
63    public function current(): mixed
64    {
65        return $this->currentNode;
66    }
67
68    /**
69     * valid
70     *
71     * @return bool
72     */
73    public function valid(): bool
74    {
75        return !is_null($this->currentNode);
76    }
77
78    /**
79     * rewind
80     *
81     * @throws StartNodeUnsetException
82     */
83    public function rewind(): void
84    {
85        $this->setCurrent($this->getStartNode());
86        $this->currentLevel = 0;
87    }
88
89    /**
90     * setCurrent
91     *
92     * @param  NodeType|null  $currentNode
93     * nullable because you want to set the current node to null at the end of a search, after the last node has been
94     * returned and have it initialized as null
95     */
96    protected function setCurrent(mixed $currentNode): void
97    {
98        $this->currentNode = $currentNode;
99    }
100
101    /**
102     * getStartNode
103     *
104     * @return NodeType
105     * startNode must be set before the class can do anything so throw an exception if it is not set
106     * @throws StartNodeUnsetException
107     */
108    protected function getStartNode(): mixed
109    {
110        if (!isset($this->startNode)) {
111            throw new StartNodeUnsetException();
112        }
113        return $this->startNode;
114    }
115
116    /**
117     * setStartNode
118     *
119     * @param  NodeType  $startNode
120     */
121    public function setStartNode($startNode): void
122    {
123        $this->startNode = $startNode;
124    }
125
126    /**
127     * @return void
128     */
129    abstract public function next(): void;
130
131    /**
132     * @return array<NodeType>
133     */
134    public function getNodes(): array
135    {
136        $result = [];
137        foreach ($this as $node) {
138            $result[$node->getNodeId()] = $node;
139        }
140        return $result;
141    }
142
143    /**
144     * @return bool
145     *
146     *  as an example, max levels of 2 means the first level (containing the start node) is at level 0 and the level
147     *  below that is on level 1.  So if the current level goes to 1 then we are at the max-levels
148     *  threshold.
149     *
150     * the current level is an absolute value.  If we are doing a search of ancestors, then the level
151     * above the start node is level 1.
152     */
153    protected function atMaxLevels(): bool
154    {
155        return ($this->getCurrentLevel() == $this->getMaxLevels() - 1);
156    }
157
158    /**
159     * getCurrentLevel
160     *
161     * @return int<-1, max>
162     *
163     * it is conceivable someone could want to know what level of the nodes the search is currently on while
164     * in the middle of iteration so keep this method public
165     */
166    public function getCurrentLevel(): int
167    {
168        return $this->currentLevel;
169    }
170
171    /**
172     * @param  int  $increment
173     *
174     * @return void
175     * we only want subclasses to be able to modify the current level of the search
176     */
177    protected function setCurrentLevel(int $increment): void
178    {
179        /**
180         * by using an absolute value for the current level, we are tracking the "distance away
181         * from the start node".  It is up to the subclasses to determine whether going DOWN (UP) is
182         * getting closer to or farther away from the start node.
183         *
184         * Type checker cannot know that the logic in the searches does not permit a current level of
185         * less than zero so we use the assertion
186         */
187        $newLevel = $this->currentLevel + $increment;
188        assert($newLevel >= 0);
189        $this->currentLevel = $newLevel;
190    }
191
192    /**
193     * getMaxLevels
194     *
195     * @return int
196     */
197    public function getMaxLevels(): int
198    {
199        return $this->maxLevels;
200    }
201
202    /**
203     * setMaxLevels
204     *
205     * @param  int  $maxLevels
206     *
207     * @throws SetMaxSearchLevelsException
208     *
209     * it is easy to get confused about this, but startNode is at level 0, meaning that current level uses
210     * zero-based counting.  BUT, max levels is one-based.  So if you set max levels = 3, you will get three levels
211     * of nodes which are at levels 0, 1 and 2.
212     */
213    public function setMaxLevels(int $maxLevels): void
214    {
215        if ($maxLevels < 1) {
216            throw new SetMaxSearchLevelsException($maxLevels);
217        } else {
218            $this->maxLevels = $maxLevels;
219        }
220    }
221
222    protected function invalidate(): void
223    {
224        $this->currentNode = null;
225        $this->currentLevel = 0;
226    }
227}