Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
24 / 24 |
|
100.00% |
14 / 14 |
CRAP | |
100.00% |
1 / 1 |
SearchAbstract | |
100.00% |
24 / 24 |
|
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% |
2 / 2 |
|
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 int <-1, max> |
42 | * current level of -1 is 'undefined' |
43 | */ |
44 | private int $currentLevel = -1; |
45 | |
46 | /** |
47 | * key |
48 | * @return non-negative-int|null |
49 | */ |
50 | public function key(): int|null |
51 | { |
52 | return $this->current()?->getNodeId(); |
53 | } |
54 | |
55 | /** |
56 | * current |
57 | * @return NodeType|null |
58 | */ |
59 | public function current(): mixed |
60 | { |
61 | return $this->currentNode; |
62 | } |
63 | |
64 | /** |
65 | * valid |
66 | * @return bool |
67 | */ |
68 | public function valid(): bool |
69 | { |
70 | return !is_null($this->currentNode); |
71 | } |
72 | |
73 | /** |
74 | * rewind |
75 | * @throws StartNodeUnsetException |
76 | */ |
77 | public function rewind(): void |
78 | { |
79 | $this->setCurrent($this->getStartNode()); |
80 | $this->currentLevel = 0; |
81 | } |
82 | |
83 | /** |
84 | * setCurrent |
85 | * @param NodeType|null $currentNode |
86 | * nullable because you want to set the current node to null at the end of a search, after the last node has been |
87 | * returned and have it initialized as null |
88 | */ |
89 | protected function setCurrent(mixed $currentNode): void |
90 | { |
91 | $this->currentNode = $currentNode; |
92 | } |
93 | |
94 | /** |
95 | * getStartNode |
96 | * @return NodeType |
97 | * startNode must be set before the class can do anything so throw an exception if it is not set |
98 | * @throws StartNodeUnsetException |
99 | */ |
100 | public function getStartNode(): mixed |
101 | { |
102 | if (!isset($this->startNode)) { |
103 | throw new StartNodeUnsetException(); |
104 | } |
105 | return $this->startNode; |
106 | } |
107 | |
108 | /** |
109 | * setStartNode |
110 | * @param NodeType $startNode |
111 | */ |
112 | public function setStartNode($startNode): void |
113 | { |
114 | $this->startNode = $startNode; |
115 | } |
116 | |
117 | /** |
118 | * @return void |
119 | */ |
120 | abstract public function next(): void; |
121 | |
122 | /** |
123 | * @return array<NodeType> |
124 | */ |
125 | public function getNodes(): array |
126 | { |
127 | $result = []; |
128 | foreach ($this as $node) { |
129 | $result[$node->getNodeId()] = $node; |
130 | } |
131 | return $result; |
132 | } |
133 | |
134 | /** |
135 | * @return bool |
136 | * |
137 | * as an example, max levels of 2 means the first level (containing the start node) is at level 0 and the level |
138 | * below that is on level 1. So if the current level goes to 1 then we are at the max-levels |
139 | * threshold. |
140 | */ |
141 | protected function atMaxLevels(): bool |
142 | { |
143 | return ($this->getCurrentLevel() == $this->getMaxLevels() - 1); |
144 | } |
145 | |
146 | /** |
147 | * getCurrentLevel |
148 | * @return int<-1, max> |
149 | * |
150 | * it is conceivable someone could want to know what level of the nodes the search is currently on while |
151 | * in the middle of iteration so keep this method public |
152 | */ |
153 | public function getCurrentLevel(): int |
154 | { |
155 | return $this->currentLevel; |
156 | } |
157 | |
158 | /** |
159 | * @param Direction $direction |
160 | * @return void |
161 | * we only want subclasses to be able to modify the current level of the search |
162 | */ |
163 | protected function setCurrentLevel(Direction $direction): void |
164 | { |
165 | /** |
166 | * feels backwards but moving down in the search increases the level. Because Direction::DOWN is defined as |
167 | * -1, we want to subtract it from, not add it to, the current level. |
168 | * |
169 | * type checker does not know that a value of -1 is an initialization condition and that the current level |
170 | * must be >= 0 in the middle of a search |
171 | */ |
172 | assert($this->currentLevel >= 0); |
173 | $this->currentLevel -= $direction->value; |
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 = -1; |
207 | } |
208 | } |