RangeDifferencer::createRangeDifference3()   B
last analyzed

Complexity

Conditions 6
Paths 12

Size

Total Lines 59
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 6.001

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 35
c 1
b 0
f 0
nc 12
nop 7
dl 0
loc 59
ccs 32
cts 33
cp 0.9697
crap 6.001
rs 8.7377

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
2
/**
3
 * (c) Steve Nebes <[email protected]>
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
declare(strict_types=1);
10
11
namespace SN\RangeDifferencer;
12
13
/**
14
 * A RangeDifferencer finds the differences between two or three RangeComparatorInterfaces.
15
 *
16
 * To use the differencer, clients provide an RangeComparatorInterface that breaks their input data into a sequence of
17
 * comparable entities. The differencer returns the differences among these sequences as an array of RangeDifference
18
 * objects (findDifferences methods). Every RangeDifference represents a single kind of difference and the
19
 * corresponding ranges of the underlying comparable entities in the left, right, and optionally ancestor sides.
20
 *
21
 * Alternatively, the findRanges methods not only return objects for the differing ranges but for non-differing ranges
22
 * too.
23
 *
24
 * The algorithm used is an objectified version of one described in: A File Comparison Program, by Webb Miller and
25
 * Eugene W. Myers, Software Practice and Experience, Vol. 15, Nov. 1985.
26
 */
27
final class RangeDifferencer
28
{
29
    /**
30
     * Prevent class instantiation.
31
     */
32
    private function __construct()
33
    {
34
    }
35
36
    /**
37
     * Finds the differences between two RangeComparatorInterfaces. The differences are returned as an array of
38
     * RangeDifferences. If no differences are detected an empty array is returned.
39
     *
40
     * @param RangeComparatorInterface $left
41
     * @param RangeComparatorInterface $right
42
     * @return RangeDifference[]
43
     */
44 16
    public static function findDifferences(RangeComparatorInterface $left, RangeComparatorInterface $right): array
45
    {
46 16
        return RangeComparatorLCS::findDifferences($left, $right);
47
    }
48
49
    /**
50
     * Finds the differences among three RangeComparatorInterfaces. The differences are returned as a list of
51
     * RangeDifferences. If no differences are detected an empty list is returned. If the ancestor range comparator is
52
     * null, a two-way comparison is performed.
53
     *
54
     * @param RangeComparatorInterface $ancestor
55
     * @param RangeComparatorInterface $left
56
     * @param RangeComparatorInterface $right
57
     * @return RangeDifference[]
58
     */
59 1
    public static function findDifferences3(
60
        RangeComparatorInterface $ancestor,
61
        RangeComparatorInterface $left,
62
        RangeComparatorInterface $right
63
    ): array {
64 1
        $leftAncestorScript = [];
65 1
        $rightAncestorScript = static::findDifferences($ancestor, $right);
66
67 1
        if (!empty($rightAncestorScript)) {
68 1
            $leftAncestorScript = static::findDifferences($ancestor, $left);
69
        }
70
71 1
        if (empty($rightAncestorScript) || empty($leftAncestorScript)) {
72
            return [];
73
        }
74
75 1
        $myIter = new DifferencesIterator($rightAncestorScript);
76 1
        $yourIter = new DifferencesIterator($leftAncestorScript);
77
78
        // Add a sentinel.
79 1
        $diff3 = [];
80 1
        $diff3[] = new RangeDifference(RangeDifference::ERROR);
81
82
        // Combine the two two-way edit scripts into one.
83 1
        while (null !== $myIter->getDifference() || null !== $yourIter->getDifference()) {
84 1
            $myIter->removeAll();
85 1
            $yourIter->removeAll();
86
87 1
            if (null === $myIter->getDifference()) {
88 1
                $startThread = $yourIter;
89 1
            } elseif (null === $yourIter->getDifference()) {
90
                $startThread = $myIter;
91
            } else {
92
                // Not at end of both scripts take the lowest range.
93 1
                if ($myIter->getDifference()->getLeftStart() < $yourIter->getDifference()->getLeftStart()) {
94
                    // 2 -> common (Ancestor)
95 1
                    $startThread = $myIter;
96 1
                } elseif ($myIter->getDifference()->getLeftStart() > $yourIter->getDifference()->getLeftStart()) {
97
                    $startThread = $yourIter;
98
                } else {
99 1
                    if ($myIter->getDifference()->getLeftLength() === 0 &&
100 1
                        $yourIter->getDifference()->getLeftLength() === 0) {
101
                        // Insertion into the same position is conflict.
102 1
                        $changeRangeStart = $myIter->getDifference()->getLeftStart();
103 1
                        $changeRangeEnd = $myIter->getDifference()->getLeftEnd();
104
105 1
                        $myIter->next();
106 1
                        $yourIter->next();
107
108 1
                        $diff3[] = static::createRangeDifference3(
109 1
                            $myIter, $yourIter, $diff3, $right, $left, $changeRangeStart, $changeRangeEnd);
110 1
                        continue;
111
                    } elseif (0 === $myIter->getDifference()->getLeftLength()) {
112
                        // Insertion into a position, and modification to the next line, is not conflict.
113
                        $startThread = $myIter;
114
                    } elseif (0 === $yourIter->getDifference()->getLeftLength()) {
115
                        $startThread = $yourIter;
116
                    } else {
117
                        // Modifications to overlapping lines is conflict.
118
                        $startThread = $myIter;
119
                    }
120
                }
121
            }
122
123 1
            $changeRangeStart = $startThread->getDifference()->getLeftStart();
124 1
            $changeRangeEnd = $startThread->getDifference()->getLeftEnd();
125 1
            $startThread->next();
126
127
            // Check for overlapping changes with other thread. Merge overlapping changes with this range.
128 1
            $other = $startThread->other($myIter, $yourIter);
129
130 1
            while (null !== $other->getDifference() && $other->getDifference()->getLeftStart() < $changeRangeEnd) {
131
                $newMax = $other->getDifference()->getLeftEnd();
132
                $other->next();
133
134
                if ($newMax > $changeRangeEnd) {
135
                    $changeRangeEnd = $newMax;
136
                    $other = $other->other($myIter, $yourIter);
137
                }
138
            }
139
140 1
            $diff3[] = static::createRangeDifference3(
141 1
                $myIter, $yourIter, $diff3, $right, $left, $changeRangeStart, $changeRangeEnd);
142
        }
143
144
        // Remove sentinel.
145 1
        array_shift($diff3);
146
147 1
        return $diff3;
148
    }
149
150
    /**
151
     * Finds the differences among two RangeComparatorInterfaces. In contrast to findDifferences, the result contains
152
     * RangeDifference elements for non-differing ranges too.
153
     *
154
     * @param RangeComparatorInterface $left
155
     * @param RangeComparatorInterface $right
156
     * @return RangeDifference[]
157
     */
158 1
    public static function findRanges(RangeComparatorInterface $left, RangeComparatorInterface $right): array
159
    {
160 1
        $in = static::findDifferences($left, $right);
161 1
        $out = [];
162
163 1
        $mStart = 0;
164 1
        $yStart = 0;
165
166 1
        for ($i = 0, $iMax = \count($in); $i < $iMax; $i++) {
167 1
            $es = $in[$i];
168 1
            $rd = new RangeDifference(
169 1
                RangeDifference::NOCHANGE,
170 1
                $mStart, $es->getRightStart() - $mStart,
171 1
                $yStart, $es->getLeftStart() - $yStart);
172
173 1
            if (0 !== $rd->getMaxLength()) {
174 1
                $out[] = $rd;
175
            }
176
177 1
            $out[] = $es;
178
179 1
            $mStart = $es->getRightEnd();
180 1
            $yStart = $es->getLeftEnd();
181
        }
182
183 1
        $rd = new RangeDifference(RangeDifference::NOCHANGE,
184 1
            $mStart, $right->getRangeCount() - $mStart,
185 1
            $yStart, $left->getRangeCount() - $yStart);
186
187 1
        if ($rd->getMaxLength() > 0) {
188 1
            $out[] = $rd;
189
        }
190
191 1
        return $out;
192
    }
193
194
    /**
195
     * Finds the differences among three RangeComparatorInterfaces. In contrast to findDifferences, the result contains
196
     * RangeDifference elements for non-differing ranges too. If the ancestor range comparator is null, a two-way
197
     * comparison is performed.
198
     *
199
     * @param RangeComparatorInterface $ancestor
200
     * @param RangeComparatorInterface $left
201
     * @param RangeComparatorInterface $right
202
     * @return RangeDifference[]
203
     */
204
    public static function findRanges3(
205
        RangeComparatorInterface $ancestor,
206
        RangeComparatorInterface $left,
207
        RangeComparatorInterface $right): array
208
    {
209
        $in = static::findDifferences3($ancestor, $left, $right);
210
        $out = [];
211
212
        $mStart = 0;
213
        $yStart = 0;
214
        $aStart = 0;
215
216
        for ($i = 0, $iMax = \count($in); $i < $iMax; $i++) {
217
            $es = $in[$i];
218
            $rd = new RangeDifference(RangeDifference::NOCHANGE,
219
                $mStart, $es->getRightStart() - $mStart,
220
                $yStart, $es->getLeftStart() - $yStart,
221
                $aStart, $es->getAncestorStart() - $aStart);
222
223
            if ($rd->getMaxLength() > 0) {
224
                $out[] = $rd;
225
            }
226
227
            $out[] = $es;
228
229
            $mStart = $es->getRightEnd();
230
            $yStart = $es->getLeftEnd();
231
            $aStart = $es->getAncestorEnd();
232
        }
233
234
        $rd = new RangeDifference(RangeDifference::NOCHANGE,
235
            $mStart, $right->getRangeCount() - $mStart,
236
            $yStart, $left->getRangeCount() - $yStart,
237
            $aStart, $ancestor->getRangeCount() - $aStart);
238
239
        if ($rd->getMaxLength() > 0) {
240
            $out[] = $rd;
241
        }
242
243
        return $out;
244
    }
245
246
    /**
247
     * @param DifferencesIterator      $myIter
248
     * @param DifferencesIterator      $yourIter
249
     * @param array                    $diff3
250
     * @param RangeComparatorInterface $right
251
     * @param RangeComparatorInterface $left
252
     * @param int                      $changeRangeStart
253
     * @param int                      $changeRangeEnd
254
     * @return RangeDifference
255
     */
256 1
    private static function createRangeDifference3(
257
        DifferencesIterator $myIter,
258
        DifferencesIterator $yourIter,
259
        array &$diff3,
260
        RangeComparatorInterface $right,
261
        RangeComparatorInterface $left,
262
        int $changeRangeStart,
263
        int $changeRangeEnd
264
    ): RangeDifference {
265 1
        $kind = RangeDifference::ERROR;
266
        /** @var RangeDifference $last */
267 1
        $last = $diff3[\count($diff3) - 1];
268
269
        // At least one range array must be non-empty.
270 1
        assert(true === ($myIter->getCount() !== 0 || $yourIter->getCount() !== 0));
271
272
        // Find corresponding lines to changeRangeStart/End in right and left.
273 1
        if ($myIter->getCount() === 0) {
274
            // Only left changed.
275 1
            $rightStart = $changeRangeStart - $last->getAncestorEnd() + $last->getRightEnd();
276 1
            $rightEnd = $changeRangeEnd - $last->getAncestorEnd() + $last->getRightEnd();
277 1
            $kind = RangeDifference::LEFT;
278
        } else {
279 1
            $myRange = $myIter->getRange();
280 1
            $f = $myRange[0];
281 1
            $l = $myRange[\count($myRange) - 1];
282 1
            $rightStart = $changeRangeStart - $f->getLeftStart() + $f->getRightStart();
283 1
            $rightEnd = $changeRangeEnd - $l->getLeftEnd() + $l->getRightEnd();
284
        }
285
286 1
        if ($yourIter->getCount() === 0) {
287
            // Only right changed.
288 1
            $leftStart = $changeRangeStart - $last->getAncestorEnd() + $last->getLeftEnd();
289 1
            $leftEnd = $changeRangeEnd - $last->getAncestorEnd() + $last->getLeftEnd();
290 1
            $kind = RangeDifference::RIGHT;
291
        } else {
292 1
            $yourRange = $yourIter->getRange();
293 1
            $f = $yourRange[0];
294 1
            $l = $yourRange[\count($yourRange) - 1];
295 1
            $leftStart = $changeRangeStart - $f->getLeftStart() + $f->getRightStart();
296 1
            $leftEnd = $changeRangeEnd - $l->getLeftEnd() + $l->getRightEnd();
297
        }
298
299 1
        if ($kind === RangeDifference::ERROR) {
300
            // Overlapping change (conflict) -> compare the changed ranges.
301 1
            if (static::rangeSpansEqual(
302 1
                $right, $rightStart, $rightEnd - $rightStart,
303 1
                $left, $leftStart, $leftEnd - $leftStart)) {
304
                $kind = RangeDifference::ANCESTOR;
305
            } else {
306 1
                $kind = RangeDifference::CONFLICT;
307
            }
308
        }
309
310 1
        return new RangeDifference(
311 1
            $kind,
312 1
            $rightStart, $rightEnd - $rightStart,
313 1
            $leftStart, $leftEnd - $leftStart,
314 1
            $changeRangeStart, $changeRangeEnd - $changeRangeStart);
315
    }
316
317
    /**
318
     * Tests whether right and left changed in the same way.
319
     *
320
     * @param RangeComparatorInterface $right
321
     * @param int                      $rightStart
322
     * @param int                      $rightLength
323
     * @param RangeComparatorInterface $left
324
     * @param int                      $leftStart
325
     * @param int                      $leftLength
326
     * @return bool
327
     */
328 1
    private static function rangeSpansEqual(
329
        RangeComparatorInterface $right,
330
        int $rightStart,
331
        int $rightLength,
332
        RangeComparatorInterface $left,
333
        int $leftStart,
334
        int $leftLength
335
    ): bool {
336 1
        if ($rightLength === $leftLength) {
337
            for ($i = 0; $i < $rightLength; $i++) {
338
                if (!static::rangesEqual($right, $rightStart + $i, $left, $leftStart + $i)) {
339
                    break;
340
                }
341
            }
342
343
            if ($i === $rightLength) {
344
                return true;
345
            }
346
        }
347
348 1
        return false;
349
    }
350
351
    /**
352
     * Tests if two ranges are equal.
353
     *
354
     * @param RangeComparatorInterface $a
355
     * @param int                      $ai
356
     * @param RangeComparatorInterface $b
357
     * @param int                      $bi
358
     * @return bool
359
     */
360
    private static function rangesEqual(
361
        RangeComparatorInterface $a,
362
        int $ai,
363
        RangeComparatorInterface $b,
364
        int $bi
365
    ): bool {
366
        return $a->rangesEqual($ai, $b, $bi);
367
    }
368
}
369