Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
SearchAbstract
100.00% covered (success)
100.00%
24 / 24
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%
2 / 2
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 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}