Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
25 / 25 |
|
100.00% |
14 / 14 |
CRAP | |
100.00% |
1 / 1 |
SearchAbstract | |
100.00% |
25 / 25 |
|
100.00% |
14 / 14 |
17 | |
100.00% |
1 / 1 |
key | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
current | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
valid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
rewind | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setCurrent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStartNode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setStartNode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
next | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getNodes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
atMaxLevels | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCurrentLevel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCurrentLevel | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getMaxLevels | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setMaxLevels | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
invalidate | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * @author: Doug Wilbourne (dougwilbourne@gmail.com) |
5 | */ |
6 | |
7 | declare(strict_types=1); |
8 | |
9 | namespace pvc\struct\treesearch; |
10 | |
11 | use pvc\interfaces\struct\treesearch\NodeSearchableInterface; |
12 | use pvc\interfaces\struct\treesearch\SearchInterface; |
13 | use pvc\struct\treesearch\err\SetMaxSearchLevelsException; |
14 | use pvc\struct\treesearch\err\StartNodeUnsetException; |
15 | |
16 | /** |
17 | * Class SearchAbstract |
18 | * @template NodeType of NodeSearchableInterface |
19 | * |
20 | * @implements SearchInterface<NodeType> |
21 | */ |
22 | abstract 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 | } |