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 * @implements SearchInterface<NodeType>
20 */
21abstract 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}