Passed
Push — master ( 6fc144...1b9765 )
by Vitaly
07:03
created

StringConditionTree::getLongestMatchingPrefix()   B

Complexity

Conditions 9
Paths 24

Size

Total Lines 54
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 54
rs 7.255
c 0
b 0
f 0
cc 9
eloc 24
nc 24
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types = 1);
2
/**
3
 * Created by Vitaly Iegorov <[email protected]>.
4
 * on 02.03.17 at 13:25
5
 */
6
namespace samsonframework\stringconditiontree;
7
use phpDocumentor\Reflection\Types\Integer;
8
9
/**
10
 * Class StringConditionTree
11
 *
12
 * @author Vitaly Egorov <[email protected]>
13
 */
14
class StringConditionTree
15
{
16
    /** Tree node root element identifier, needed for recursion */
17
    const ROOT_NAME = '';
18
19
    /** Final tree node branch identifier */
20
    const SELF_NAME = '@self';
21
22
    /** String parameter start marker */
23
    const PARAMETER_START = '{';
24
25
    /** String parameter end marker */
26
    const PARAMETER_END = '}';
27
28
    /** @var TreeNode Resulting collection for debugging */
29
    protected $debug;
30
31
    /** @var string Parametrized string start marker */
32
    protected $parameterStartMarker = self::PARAMETER_START;
33
34
    /** @var string Parametrized string end marker */
35
    protected $parameterEndMarker = self::PARAMETER_END;
36
37
    /**
38
     * StringConditionTree constructor.
39
     *
40
     * @param string $parameterStartMarker Parametrized string start marker
41
     * @param string $parameterEndMarker Parametrized string end marker
42
     */
43
    public function __construct(string $parameterStartMarker = self::PARAMETER_START, string $parameterEndMarker = self::PARAMETER_END)
44
    {
45
        $this->parameterStartMarker = $parameterStartMarker;
46
        $this->parameterEndMarker = $parameterEndMarker;
47
    }
48
49
    /**
50
     * Build similarity strings tree.
51
     *
52
     * @param array $input Collection of strings
53
     *
54
     * @return TreeNode Resulting similarity strings tree
55
     */
56
    public function process(array $input): TreeNode
57
    {
58
        /**
59
         * We need to find first matching character that present at least at one two string
60
         * to start building tree. Otherwise there is no reason to build tree.
61
         */
62
        $this->innerProcessor(self::ROOT_NAME, $input, $this->debug = new TreeNode());
63
64
        $this->debug = $this->debug->children[self::ROOT_NAME];
65
66
        return $this->debug;
67
    }
68
69
    /**
70
     * Prefix length counter for array sorting callback to sort by prefix length and put
71
     * parametrized prefixed at the end.
72
     *
73
     * @param string $prefix Prefix string
74
     *
75
     * @return int Prefix length
76
     */
77
    protected function prefixLength(string $prefix): int
78
    {
79
        return strpos($prefix, self::PARAMETER_START) !== false ? PHP_INT_MAX : strlen($prefix);
80
    }
81
82
    /**
83
     * Sort array by key string lengths.
84
     *
85
     * @param array $input Input array for sorting
86
     * @param int   $order Sorting order
87
     */
88
    protected function sortArrayByKeys(array &$input, int $order = SORT_ASC)
89
    {
90
        array_multisort(array_map([$this, 'prefixLength'], array_keys($input)), $order, $input);
0 ignored issues
show
Bug introduced by
array_map(array($this, '...'), array_keys($input)) cannot be passed to array_multisort() as the parameter $arr expects a reference.
Loading history...
91
    }
92
93
    /**
94
     * Add only unique value to array.
95
     *
96
     * @param mixed $value  Unique value
97
     * @param array $array  Array for adding unique value
98
     * @param bool  $strict Strict uniqueness check
99
     *
100
     * @see in_array();
101
     *
102
     * @return bool True if unique value was added
103
     */
104
    protected function addUniqueToArray($value, &$array, bool $strict = true)
105
    {
106
        // Create array if not array is passed
107
        if (!is_array($array)) {
108
            $array = [];
109
        }
110
111
        // Add value to array if it is unique
112
        if (!in_array($value, $array, $strict)) {
113
            $array[] = $value;
114
115
            return true;
116
        } else { // Value is already in array
117
            return false;
118
        }
119
    }
120
121
    /**
122
     * Find longest matching prefix between two strings.
123
     *
124
     * @param string $initialString  Initial string
125
     * @param string $comparedString Compared string
126
     *
127
     * @return string Longest matching prefix
128
     */
129
    protected function getLongestMatchingPrefix(string $initialString, string $comparedString): string
130
    {
131
        // Iterate and compare how string matches
132
        $longestPrefix = '';
133
134
        // Define shortest & longest strings to avoid errors
135
        $initialLength = strlen($initialString);
136
        $comparedLength = strlen($comparedString);
137
138
        // Define shortest and longest strings to avoid character matching errors
139
        $shortestString = $initialLength < $comparedLength ? $initialString : $comparedString;
140
        $longestString = $initialLength >= $comparedLength ? $initialString : $comparedString;
141
142
        // Iterate initial string characters
143
        $isPattern = false;
144
        $parametrizedPrefix = '';
145
        for ($z = 0, $length = strlen($shortestString); $z < $length; $z++) {
146
            // Pattern support
147
            // TODO: Probably can be optimized
148
            if ($isPattern || $shortestString{$z} === $this->parameterStartMarker) {
149
                $isPattern = true;
150
151
                // Concatenate longest matching prefix
152
                $parametrizedPrefix .= $shortestString{$z};
153
154
                // Compare characters with compared string
155
                if ($shortestString{$z} !== $longestString{$z}) {
156
                    // Clear pattern as pattern characters mismatch
157
                    $parametrizedPrefix = '';
0 ignored issues
show
Unused Code introduced by
$parametrizedPrefix is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
158
                    break;
159
                }
160
161
                // If pattern id closed unset flag fro special behaviour
162
                if ($shortestString{$z} === $this->parameterEndMarker) {
163
                    // If parametrized part ended append to longest matching prefix
164
                    $longestPrefix .= $parametrizedPrefix;
165
                    // Clear parametrized prefix as we can have other parametrized parts
166
                    $parametrizedPrefix = '';
167
                    // Reset parametrized flag
168
                    $isPattern = false;
169
                }
170
            } else {
171
                // Compare characters with compared string
172
                if ($shortestString{$z} !== $longestString{$z}) {
173
                    break; // Exit on first mismatching character
174
                }
175
176
                // Concatenate matching part of two strings
177
                $longestPrefix .= $initialString{$z};
178
            }
179
        }
180
181
        return $longestPrefix;
182
    }
183
184
    /**
185
     * Remove key string from the beginning of all sub-array strings.
186
     *
187
     * @param array  $array Input array of key => [keyStrings...]
188
     *
189
     * @param string $selfMarker Marker for storing self pointer
190
     *
191
     * @return array Processed array with removed keys from beginning of sub arrays
192
     */
193
    protected function removeKeyFromArrayStrings(array $array, string $selfMarker): array
194
    {
195
        $result = [];
196
        /** @var string[] $values */
197
        foreach ($array as $key => $values) {
198
            $lmpLength = strlen((string)$key);
199
            foreach ($values as $string) {
200
                $newString = substr($string, $lmpLength);
201
202
                if ($newString === false || $newString === '' || $string === $selfMarker) {
203
                    $result[$key][] = $selfMarker;
204
                } else {
205
                    $result[$key][] = $newString;
206
                }
207
            }
208
        }
209
210
        return $result;
211
    }
212
213
    /**
214
     * Find all duplication of source array values in compared array and remove them.
215
     *
216
     * @param array $source Source array
217
     * @param array $compared Compared array for filtering duplicates
218
     */
219
    protected function removeDuplicatesInSubArray(array $source, array &$compared)
220
    {
221
        foreach ($source as $value) {
222
            foreach ($compared as $key => &$subValue) {
223
                if ($subValue === $value) {
224
                    unset($compared[$key]);
225
                }
226
            }
227
        }
228
    }
229
230
    /**
231
     * Recursive string similarity tree builder.
232
     *
233
     * @param string   $prefix
234
     * @param array    $input
235
     * @param TreeNode $result
236
     * @param string   $selfMarker
237
     */
238
    protected function innerProcessor(string $prefix, array $input, TreeNode $result, $selfMarker = self::SELF_NAME)
239
    {
240
        // Create tree node
241
        $newChild = $result->append($prefix);
242
243
        /**
244
         * Iterate all combinations of strings and group by LMP
245
         */
246
        $longestPrefixes = [];
247
        for ($i = 0, $count = count($input); $i < $count; $i++) {
248
            for ($j = $i + 1; $j < $count; $j++) {
249
                $longestMatchedPrefix = $this->getLongestMatchingPrefix($input[$i], $input[$j]);
250
251
                // We have found at least one matching character between strings
252
                if ($longestMatchedPrefix !== '') {
253
                    $this->addUniqueToArray($input[$i], $longestPrefixes[$longestMatchedPrefix]);
254
                    $this->addUniqueToArray($input[$j], $longestPrefixes[$longestMatchedPrefix]);
255
                }
256
            }
257
        }
258
259
        if (array_key_exists('{param}-{parameter}', $longestPrefixes)) {
260
            var_dump(1);
0 ignored issues
show
Security Debugging Code introduced by
var_dump(1); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
261
        }
262
263
264
        /**
265
         * Sort LMPs(array keys) ascending by key length
266
         */
267
        $this->sortArrayByKeys($longestPrefixes);
268
269
        /**
270
         * Iterate all sorted LMP strings and remove duplicates from LMP string ordered lower
271
         */
272
        $keys = array_keys($longestPrefixes);
273
        for ($i = 0, $length = count($keys); $i < $length; $i++) {
274
            for ($j = $i + 1; $j < $length; $j++) {
275
                $this->removeDuplicatesInSubArray($longestPrefixes[$keys[$i]], $longestPrefixes[$keys[$j]]);
276
            }
277
        }
278
279
        // Remove empty LMPs as they are included in smaller LMPs
280
        $longestPrefixes = array_filter($longestPrefixes);
281
282
        /**
283
         * Search for input string that do not have LMP, and add missing as LMP
284
         */
285
        foreach ($input as $string) {
286
            $found = false;
287
288
            if ($string !== $selfMarker) {
289
                foreach ($longestPrefixes as $strings) {
290
                    if (in_array($string, $strings, true)) {
291
                        $found = true;
292
                        break;
293
                    }
294
                }
295
296
                if (!$found) {
297
                    $longestPrefixes[$string] = [$selfMarker];
298
                }
299
            }
300
        }
301
302
        /**
303
         * After filtering LMPs remove LMP from matched string arrays
304
         */
305
        $longestPrefixes = $this->removeKeyFromArrayStrings($longestPrefixes, $selfMarker);
306
307
        /**
308
         * Sort LMPs(array keys) ascending by key length
309
         */
310
        $this->sortArrayByKeys($longestPrefixes);
311
312
        /**
313
         * If we have self marker as an input string - create LMP for it
314
         */
315
        if (in_array($selfMarker, $input, true)) {
316
            $longestPrefixes = array_merge([$selfMarker => []], $longestPrefixes);
317
        }
318
319
        /**
320
         * Recursively iterate current level LMPs
321
         */
322
        foreach ($longestPrefixes as $longestPrefix => $strings) {
323
            $this->innerProcessor((string)$longestPrefix, $strings, $newChild);
324
        }
325
    }
326
}
327