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 | * @implements SearchInterface<NodeType> |
20 | */ |
21 | abstract class SearchAbstract implements SearchInterface |
22 | { |
23 | /** |
24 | * @var NodeType |
25 | */ |
26 | protected mixed $startNode; |
27 | |
28 | /** |
29 | * @var NodeType|null |
30 | */ |
31 | protected mixed $currentNode = null; |
32 | |
33 | /** |
34 | * @var int |
35 | * |
36 | * maximum depth to which we are allowed to traverse the tree. |
37 | */ |
38 | private int $maxLevels = PHP_INT_MAX; |
39 | |
40 | /** |
41 | * @var non-negative-int int |
42 | */ |
43 | private int $currentLevel = 0; |
44 | |
45 | /** |
46 | * key |
47 | * @return non-negative-int|null |
48 | */ |
49 | public function key(): int|null |
50 | { |
51 | return $this->current()?->getNodeId(); |
52 | } |
53 | |
54 | /** |
55 | * current |
56 | * @return NodeType|null |
57 | */ |
58 | public function current(): mixed |
59 | { |
60 | return $this->currentNode; |
61 | } |
62 | |
63 | /** |
64 | * valid |
65 | * @return bool |
66 | */ |
67 | public function valid(): bool |
68 | { |
69 | return !is_null($this->currentNode); |
70 | } |
71 | |
72 | /** |
73 | * rewind |
74 | * @throws StartNodeUnsetException |
75 | */ |
76 | public function rewind(): void |
77 | { |
78 | $this->setCurrent($this->getStartNode()); |
79 | $this->currentLevel = 0; |
80 | } |
81 | |
82 | /** |
83 | * setCurrent |
84 | * @param NodeType|null $currentNode |
85 | * nullable because you want to set the current node to null at the end of a search, after the last node has been |
86 | * returned and have it initialized as null |
87 | */ |
88 | protected function setCurrent(mixed $currentNode): void |
89 | { |
90 | $this->currentNode = $currentNode; |
91 | } |
92 | |
93 | /** |
94 | * getStartNode |
95 | * @return NodeType |
96 | * startNode must be set before the class can do anything so throw an exception if it is not set |
97 | * @throws StartNodeUnsetException |
98 | */ |
99 | public function getStartNode(): mixed |
100 | { |
101 | if (!isset($this->startNode)) { |
102 | throw new StartNodeUnsetException(); |
103 | } |
104 | return $this->startNode; |
105 | } |
106 | |
107 | /** |
108 | * setStartNode |
109 | * @param NodeType $startNode |
110 | */ |
111 | public function setStartNode($startNode): void |
112 | { |
113 | $this->startNode = $startNode; |
114 | } |
115 | |
116 | /** |
117 | * @return void |
118 | */ |
119 | abstract public function next(): void; |
120 | |
121 | /** |
122 | * @return array<NodeType> |
123 | */ |
124 | public function getNodes(): array |
125 | { |
126 | $result = []; |
127 | foreach ($this as $node) { |
128 | $result[$node->getNodeId()] = $node; |
129 | } |
130 | return $result; |
131 | } |
132 | |
133 | /** |
134 | * @return bool |
135 | * |
136 | * as an example, max levels of 2 means the first level (containing the start node) is at level 0 and the level |
137 | * below that is on level 1. So if the current level goes to 1 then we are at the max-levels |
138 | * threshold. |
139 | */ |
140 | protected function atMaxLevels(): bool |
141 | { |
142 | return ($this->getCurrentLevel() == $this->getMaxLevels() - 1); |
143 | } |
144 | |
145 | /** |
146 | * getCurrentLevel |
147 | * @return int<-1, max> |
148 | * |
149 | * it is conceivable someone could want to know what level of the nodes the search is currently on while |
150 | * in the middle of iteration so keep this method public |
151 | */ |
152 | public function getCurrentLevel(): int |
153 | { |
154 | return $this->currentLevel; |
155 | } |
156 | |
157 | /** |
158 | * @param Direction $direction |
159 | * @return void |
160 | * we only want subclasses to be able to modify the current level of the search |
161 | */ |
162 | protected function setCurrentLevel(Direction $direction): void |
163 | { |
164 | /** |
165 | * feels backwards but moving down in the search increases the level. Because Direction::DOWN is defined as |
166 | * -1, we want to subtract it from, not add it to, the current level. |
167 | * |
168 | * Type checker cannot know that the logic in the searches does not permit a current level of |
169 | * -1 |
170 | */ |
171 | $newLevel = $this->currentLevel - $direction->value; |
172 | assert($newLevel >= 0); |
173 | $this->currentLevel = $newLevel; |
174 | } |
175 | |
176 | /** |
177 | * getMaxLevels |
178 | * @return int |
179 | */ |
180 | public function getMaxLevels(): int |
181 | { |
182 | return $this->maxLevels; |
183 | } |
184 | |
185 | /** |
186 | * setMaxLevels |
187 | * @param int $maxLevels |
188 | * @throws SetMaxSearchLevelsException |
189 | * |
190 | * it is easy to get confused about this, but startNode is at level 0, meaning that current level uses |
191 | * zero-based counting. BUT, max levels is one-based. So if you set max levels = 3, you will get three levels |
192 | * of nodes which are at levels 0, 1 and 2. |
193 | */ |
194 | public function setMaxLevels(int $maxLevels): void |
195 | { |
196 | if ($maxLevels < 1) { |
197 | throw new SetMaxSearchLevelsException($maxLevels); |
198 | } else { |
199 | $this->maxLevels = $maxLevels; |
200 | } |
201 | } |
202 | |
203 | protected function invalidate(): void |
204 | { |
205 | $this->currentNode = null; |
206 | $this->currentLevel = 0; |
207 | } |
208 | } |