Completed
Push — master ( d9ace6...2f86e5 )
by Magnar Ovedal
05:50 queued 03:06
created

CharTree::getBranches()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice;
6
7
use InvalidArgumentException;
8
use IteratorAggregate;
9
use Traversable;
10
11
final class CharTree implements IteratorAggregate
12
{
13
    /**
14
     * @var string|null
15
     */
16
    private $root;
17
18
    /**
19
     * @var self[]
20
     */
21
    private $branches = [];
22
23
    /**
24
     * @var bool[]
25
     */
26
    private $startsWithMemoization = [];
27
28
    /**
29
     * @var bool[]
30
     */
31
    private $containsMemoization = [];
32
33
    /**
34
     * @var CharTree[]
35
     */
36
    private static $charTreeMemoization = [];
37
38
    /**
39
     * @param string|null $root Root of the character tree. No more than 1 character long.
40
     * @param self[] $branches Branches of the character tree.
41
     */
42 14
    private function __construct(?string $root, array $branches)
43
    {
44 14
        assert($root !== null || $branches === [], 'Empty tree cannot have branches.');
45 14
        assert($root === null || mb_strlen($root) <= 1, 'Root must contain at most one character.');
46
47 14
        $this->root = $root;
48 14
        $this->branches = $branches;
49 14
    }
50
51
    /**
52
     * @param string|null $root Root of the character tree. No more than 1 character long.
53
     * @param self[] $branches Branches of the character tree.
54
     */
55 35
    private static function construct(?string $root, array $branches): self
56
    {
57 35
        if ($root === null) {
58 27
            assert($branches === [], 'Empty tree cannot have branches.');
59 27
            if (!isset(self::$charTreeMemoization['null'])) {
60 1
                self::$charTreeMemoization['null'] = new self($root, $branches);
61
            }
62 27
            return self::$charTreeMemoization['null'];
63
        }
64
65 31
        assert(mb_strlen($root) <= 1, 'Root must contain at most one character.');
66
67 31
        $hash = $root;
68 31
        ksort($branches, SORT_STRING);
69 31
        foreach ($branches as $branch) {
70
            // When PHP 7.1 is no longer supported, change to using spl_object_id.
71 23
            $branchHash = spl_object_hash($branch);
72 23
            $hash .= ';'.$branchHash;
73
        }
74
75 31
        if (!isset(self::$charTreeMemoization[$hash])) {
76 13
            self::$charTreeMemoization[$hash] = new self($root, $branches);
77
        }
78
79 31
        return self::$charTreeMemoization[$hash];
80
    }
81
82
    /**
83
     * @param string $string Root of the character tree.
84
     * @param self[] $branches Branches of the character tree.
85
     */
86 9
    public static function fromString(string $string, array $branches = []): self
87
    {
88 9
        if (1 < mb_strlen($string)) {
89
            // Maximum one character in root.
90 3
            $branches = [self::fromString(mb_substr($string, 1), $branches)];
91 3
            $string = mb_substr($string, 0, 1);
92
        }
93
94 9
        $branches = self::promoteEmptyRoots($branches);
95 9
        $branches = self::combineDuplicateRoots($branches);
96
97 9
        if (count($branches) === 1) {
98 6
            $branch = reset($branches);
99 6
            if ($branch->root === null) {
100
                // If only one branch and its root is null, throw it away.
101 3
                $branches = [];
102 4
            } elseif ($string === '') {
103
                // If root is empty string and only one branch, use it.
104 2
                return $branch;
105
            }
106
        }
107
108 9
        return self::construct($string, $branches);
109
    }
110
111
    /**
112
     * @param self[] $charTrees Character trees to combine.
113
     */
114 4
    public static function fromArray(array $charTrees): self
115
    {
116 4
        foreach ($charTrees as $charTree) {
117 3
            if ($charTree->root !== null) {
118 3
                return self::fromString('', $charTrees);
119
            }
120
        }
121
122 2
        return self::fromNothing();
123
    }
124
125
    /**
126
     * Construct empty character tree.
127
     */
128 1
    public static function fromNothing(): self
129
    {
130 1
        return self::construct(null, []);
131
    }
132
133
    /**
134
     * @param self[] $charTrees Character trees to normalize.
135
     * @return self[] Character trees where branches of trees with empty string roots have been promoted to trees.
136
     */
137 31
    private static function promoteEmptyRoots(array $charTrees): array
138
    {
139 31
        $normalized = [];
140 31
        foreach ($charTrees as $charTree) {
141 27
            if ($charTree->root === '') {
142 8
                if ($charTree->branches !== []) {
143 1
                    $normalized = array_merge($normalized, array_values($charTree->branches));
144
                } else {
145 8
                    $normalized[] = self::fromNothing();
146
                }
147
            } else {
148 27
                $normalized[] = $charTree;
149
            }
150
        }
151
152 31
        return $normalized;
153
    }
154
155
    /**
156
     * @param self[] $charTrees Character trees to normalize.
157
     * @return self[] Character trees where branches of trees with the same root have been combined.
158
     */
159 31
    private static function combineDuplicateRoots(array $charTrees): array
160
    {
161 31
        $normalized = [];
162 31
        foreach ($charTrees as $charTree) {
163 27
            if (isset($normalized[$charTree->root]) && $charTree->root !== null) {
164 21
                $normalizedCharTree = $normalized[$charTree->root];
165 21
                $normalizedBranches = array_values($normalizedCharTree->branches);
166 21
                if ($normalizedBranches === []) {
167 19
                    $normalizedBranches[] = self::fromNothing();
168
                }
169 21
                $branches = array_values($charTree->branches);
170 21
                if ($branches === []) {
171 2
                    $branches[] = self::fromNothing();
172
                }
173 21
                $charTree = self::fromString(
174 21
                    $charTree->root,
175 21
                    array_merge($normalizedBranches, $branches)
176
                );
177
            }
178 27
            $normalized[$charTree->root] = $charTree;
179
        }
180
181 31
        return $normalized;
182
    }
183
184
    /**
185
     * @return string|null Root of the character tree.
186
     */
187 1
    public function getRoot(): ?string
188
    {
189 1
        return $this->root;
190
    }
191
192
    /**
193
     * @return self[] Branches of the character tree.
194
     */
195 1
    public function getBranches(): array
196
    {
197 1
        return $this->branches;
198
    }
199
200
    /**
201
     * @param string $root Root that comes before the brances.
202
     * @return self Character tree containing the branches of this character tree that come after $root.
203
     */
204 8
    public function getBranchesAfterRoot(string $root): self
205
    {
206 8
        if ($this->root === null || $root === '') {
207 3
            return $this;
208
        }
209
210 5
        if ($this->root === $root) {
211 3
            return self::fromString('', $this->branches);
212
        }
213
214 5
        if ($this->root !== mb_substr($root, 0, mb_strlen($this->root))) {
215 1
            return self::fromNothing();
216
        }
217
218 4
        $rootTail = mb_substr($root, mb_strlen($this->root));
219 4
        $rootHead = mb_substr($rootTail, 0, 1);
220
221 4
        if (!isset($this->branches[$rootHead])) {
222 1
            return self::fromNothing();
223
        }
224
225 4
        return $this->branches[$rootHead]->getBranchesAfterRoot($rootTail);
226
    }
227
228
    /**
229
     * @param string $string String to check.
230
     * @return bool Whether the character tree contains the string.
231
     */
232 5
    public function contains(string $string): bool
233
    {
234 5
        if (!isset($this->containsMemoization[$string])) {
235 5
            $this->containsMemoization[$string] = $this->calculateContains($string);
236
        }
237
238 5
        return $this->containsMemoization[$string];
239
    }
240
241
    /**
242
     * @param string $string String to check.
243
     * @return bool Whether the character tree starts with the string.
244
     */
245 5
    public function startsWith(string $string): bool
246
    {
247 5
        if (!isset($this->startsWithMemoization[$string])) {
248 5
            $this->startsWithMemoization[$string] = $this->calculateStartsWith($string);
249
        }
250
251 5
        return $this->startsWithMemoization[$string];
252
    }
253
254
    /**
255
     * @param string $string String to check.
256
     * @return bool Whether the character tree contains the string. Memoization is not used.
257
     */
258 5
    private function calculateContains(string $string): bool
259
    {
260 5
        if ($this->startsWith($string)) {
261 4
            return true;
262
        }
263
264 3
        foreach ($this->branches as $branch) {
265 3
            if ($branch->contains($string)) {
266 3
                return true;
267
            }
268
        }
269
270 3
        return false;
271
    }
272
273
    /**
274
     * @param string $string String to check.
275
     * @return bool Whether the character tree starts with the string. Memoization is not used.
276
     */
277 10
    private function calculateStartsWith(string $string): bool
278
    {
279 10
        if ($this->root !== null && $this->root === mb_substr($string, 0, mb_strlen($this->root))) {
280 10
            $stringTail = mb_substr($string, mb_strlen($this->root));
281 10
            if ($stringTail === '') {
282 4
                return true;
283
            } else {
284 10
                foreach ($this->branches as $branch) {
285 10
                    if ($branch->startsWith($stringTail)) {
286 10
                        return true;
287
                    }
288
                }
289
            }
290
        }
291
292 8
        return false;
293
    }
294
295
    /**
296
     * @return Traversable<string> All strings in the character tree.
297
     */
298 1
    public function getIterator(): Traversable
299
    {
300 1
        if ($this->root !== null) {
301 1
            foreach ($this->branches as $branch) {
302 1
                if ($branch->root === null) {
303 1
                    yield $this->root;
304
                } else {
305 1
                    foreach ($branch as $string) {
306 1
                        yield $this->root.$string;
307
                    }
308
                }
309
            }
310
311 1
            if ($this->branches === []) {
312 1
                yield $this->root;
313
            }
314
        }
315 1
    }
316
}
317