Passed
Push — master ( c4b1f5...d4c130 )
by Magnar Ovedal
02:36
created

CharTree::__construct()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 1
nop 2
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 2
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
     * @param string|null $root Root of the character tree. No more than 1 character long.
35
     * @param self[] $branches Branches of the character tree.
36
     */
37 40
    private function __construct(?string $root, array $branches)
38
    {
39 40
        assert($root === null || mb_strlen($root) <= 1, 'Root must contain at most one character.');
40
41 40
        $this->root = $root;
42 40
        $this->branches = $branches;
43 40
    }
44
45
    /**
46
     * @param string $string Root of the character tree.
47
     * @param self[] $branches Branches of the character tree.
48
     */
49 9
    public static function fromString(string $string, array $branches = []): self
50
    {
51 9
        if (1 < mb_strlen($string)) {
52
            // Maximum one character in root.
53 3
            $branches = [self::fromString(mb_substr($string, 1), $branches)];
54 3
            $string = mb_substr($string, 0, 1);
55
        }
56
57 9
        $branches = self::promoteEmptyRoots($branches);
58 9
        $branches = self::combineDuplicateRoots($branches);
59
60 9
        if (count($branches) === 1) {
61 6
            if ($string === '') {
62
                // If root is empty and only one branch, use it.
63 3
                return reset($branches);
64
            }
65 4
            if (reset($branches)->root === null) {
66
                // If only one branch and its root is null, throw it away.
67 2
                $branches = [];
68
            }
69
        }
70
71 9
        return new self($string, $branches);
72
    }
73
74
    /**
75
     * @param self[] $charTrees Character trees to combine.
76
     */
77 4
    public static function fromArray(array $charTrees): self
78
    {
79 4
        foreach ($charTrees as $charTree) {
80 3
            if ($charTree->root !== null) {
81 3
                return self::fromString('', $charTrees);
82
            }
83
        }
84
85 2
        return self::fromNothing();
86
    }
87
88
    /**
89
     * Construct empty character tree.
90
     */
91 1
    public static function fromNothing(): self
92
    {
93 1
        return new self(null, []);
94
    }
95
96
    /**
97
     * @param self[] $charTrees Character trees to normalize.
98
     * @return self[] Character trees where branches of trees with empty string roots have been promoted to trees.
99
     */
100 36
    private static function promoteEmptyRoots(array $charTrees): array
101
    {
102 36
        $normalized = [];
103 36
        foreach ($charTrees as $charTree) {
104 32
            if ($charTree->root === '') {
105 9
                if ($charTree->branches !== []) {
106 1
                    $normalized = array_merge($normalized, array_values($charTree->branches));
107
                } else {
108 9
                    $normalized[] = self::fromNothing();
109
                }
110
            } else {
111 32
                $normalized[] = $charTree;
112
            }
113
        }
114
115 36
        return $normalized;
116
    }
117
118
    /**
119
     * @param self[] $charTrees Character trees to normalize.
120
     * @return self[] Character trees where branches of trees with the same root have been combined.
121
     */
122 36
    private static function combineDuplicateRoots(array $charTrees): array
123
    {
124 36
        $normalized = [];
125 36
        foreach ($charTrees as $charTree) {
126 32
            if (isset($normalized[$charTree->root]) && $charTree->root !== null) {
127 26
                $normalizedCharTree = $normalized[$charTree->root];
128 26
                $normalizedBranches = array_values($normalizedCharTree->branches);
129 26
                if ($normalizedBranches === []) {
130 21
                    $normalizedBranches[] = self::fromNothing();
131
                }
132 26
                $branches = array_values($charTree->branches);
133 26
                if ($branches === []) {
134 2
                    $branches[] = self::fromNothing();
135
                }
136 26
                $charTree = self::fromString(
137 26
                    $charTree->root,
138 26
                    array_merge($normalizedBranches, $branches)
139
                );
140
            }
141 32
            $normalized[$charTree->root] = $charTree;
142
        }
143
144 36
        return $normalized;
145
    }
146
147
    /**
148
     * @return string|null Root of the character tree.
149
     */
150 1
    public function getRoot(): ?string
151
    {
152 1
        return $this->root;
153
    }
154
155
    /**
156
     * @return self[] Branches of the character tree.
157
     */
158 1
    public function getBranches(): array
159
    {
160 1
        return $this->branches;
161
    }
162
163
    /**
164
     * @param int $length Length of each string in the character tree.
165
     * @return self Character tree containing the $length first characters of this character tree.
166
     */
167 5
    public function getTreeTrimmedToLength(int $length): self
168
    {
169 5
        if ($length < 0) {
170 1
            throw new InvalidArgumentException('Length must be non-negative.');
171
        }
172
173 4
        if ($this->root === null) {
174 2
            return $this;
175
        }
176
177 4
        if ($length === 0) {
178 2
            return self::fromString('');
179
        }
180
181 2
        $branches = [];
182 2
        $rootLength = mb_strlen($this->root);
183 2
        if ($rootLength < $length) {
184 2
            $branchLength = $length-$rootLength;
185 2
            foreach ($this->branches as $branch) {
186 2
                $branchTree = $branch->getTreeTrimmedToLength($branchLength);
187 2
                if ($branchTree->root !== null) {
188 2
                    $branches[] = $branchTree;
189
                }
190
            }
191
192 2
            if ($branches === []) {
193 2
                return self::fromNothing();
194
            }
195
        }
196
197 1
        return self::fromString($this->root, $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 6
                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 10
        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