Completed
Push — master ( 5522ff...bf2c42 )
by Magnar Ovedal
54:00 queued 48:54
created

CharTree   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 274
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 49
eloc 98
dl 0
loc 274
ccs 103
cts 103
cp 1
rs 8.48
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A fromNothing() 0 3 1
A fromString() 0 23 5
A fromArray() 0 9 3
A construct() 0 25 5
A combineDuplicateRoots() 0 23 6
A promoteEmptyRoots() 0 16 4
A getBranches() 0 3 1
A __construct() 0 7 3
A getRoot() 0 3 1
A startsWith() 0 7 2
A getIterator() 0 15 6
A contains() 0 7 2
A calculateStartsWith() 0 16 6
A calculateContains() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like CharTree often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CharTree, and based on these observations, apply Extract Interface, too.

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