Completed
Push — master ( 96cbf3...9bcdfc )
by Steve
03:03 queued 01:23
created

RangeDifferencer::createRangeDifference3()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 59
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 35
nc 12
nop 7
dl 0
loc 59
rs 8.7377
c 0
b 0
f 0

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
/**
4
 * (c) Steve Nebes <[email protected]>
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace SN\RangeDifferencer;
13
14
/**
15
 * A RangeDifferencer finds the differences between two or three
16
 * RangeComparatorInterfaces.
17
 *
18
 * To use the differencer, clients provide an RangeComparatorInterface that
19
 * breaks their input data into a sequence of comparable entities. The
20
 * differencer returns the differences among these sequences as an array of
21
 * RangeDifference objects (findDifferences methods). Every RangeDifference
22
 * represents a single kind of difference and the corresponding ranges of the
23
 * underlying comparable entities in the left, right, and optionally ancestor
24
 * sides.
25
 *
26
 * Alternatively, the findRanges methods not only return objects for the
27
 * differing ranges but for non-differing ranges too.
28
 *
29
 * The algorithm used is an objectified version of one described in: A File
30
 * Comparison Program, by Webb Miller and Eugene W. Myers, Software Practice
31
 * and Experience, Vol. 15, Nov. 1985.
32
 */
33
final class RangeDifferencer
34
{
35
    /**
36
     * Prevent class instantiation.
37
     */
38
    private function __construct()
39
    {
40
    }
41
42
    /**
43
     * Finds the differences between two RangeComparatorInterfaces.
44
     * The differences are returned as an array of RangeDifferences.
45
     * If no differences are detected an empty array is returned.
46
     *
47
     * @param RangeComparatorInterface $left
48
     * @param RangeComparatorInterface $right
49
     * @return RangeDifference[]
50
     */
51
    public static function findDifferences(RangeComparatorInterface $left, RangeComparatorInterface $right): array {
0 ignored issues
show
Unused Code introduced by
The parameter $left is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

51
    public static function findDifferences(/** @scrutinizer ignore-unused */ RangeComparatorInterface $left, RangeComparatorInterface $right): array {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $right is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

51
    public static function findDifferences(RangeComparatorInterface $left, /** @scrutinizer ignore-unused */ RangeComparatorInterface $right): array {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
52
        return [];
53
    }
54
55
    /**
56
     * @param RangeComparatorInterface $ancestor
57
     * @param RangeComparatorInterface $left
58
     * @param RangeComparatorInterface $right
59
     * @return RangeDifference[]
60
     */
61
    public static function findDifferences3(
62
        RangeComparatorInterface $ancestor,
63
        RangeComparatorInterface $left,
64
        RangeComparatorInterface $right
65
    ): array {
66
        if (null === $ancestor) {
67
            return static::findDifferences($left, $right);
68
        }
69
70
        $leftAncestorScript = [];
71
        $rightAncestorScript = static::findDifferences($ancestor, $right);
72
73
        if (!empty($rightAncestorScript)) {
74
            $leftAncestorScript = static::findDifferences($ancestor, $left);
75
        }
76
77
        if (empty($rightAncestorScript) || empty($leftAncestorScript)) {
78
            return [];
79
        }
80
81
        $myIterator = new DifferencesIterator($rightAncestorScript);
82
        $yourIterator = new DifferencesIterator($leftAncestorScript);
83
        $diff3 = [];
84
85
        // Add a sentinel.
86
        $diff3[] = new RangeDifference(RangeDifference::ERROR);
87
88
        // Combine the two two-way edit scripts into one.
89
        while (null !== $myIterator->getDifference() || null !== $yourIterator->getDifference()) {
90
            $myIterator->removeAll();
91
            $yourIterator->removeAll();
92
93
            if (null === $myIterator->getDifference()) {
94
                $startThread = $yourIterator;
95
            } elseif (null === $yourIterator->getDifference()) {
96
                $startThread = $myIterator;
97
            } else {
98
                // Not at end of both scripts take the lowest range.
99
                if ($myIterator->getDifference()->getLeftStart() < $yourIterator->getDifference()->getLeftStart()) {
100
                    // 2 -> common (Ancestor)
101
                    $startThread = $myIterator;
102
                } elseif ($myIterator->getDifference()->getLeftStart() >
103
                    $yourIterator->getDifference()->getLeftStart()) {
104
                    $startThread = $yourIterator;
105
                } else {
106
                    if ($myIterator->getDifference()->getLeftLength() === 0 &&
107
                        $yourIterator->getDifference()->getLeftLength() === 0) {
108
                        // Insertion into the same position is conflict.
109
                        $changeRangeStart = $myIterator->getDifference()->getLeftStart();
110
                        $changeRangeEnd = $myIterator->getDifference()->getLeftEnd();
111
112
                        $myIterator->next();
113
                        $yourIterator->next();
114
115
                        $diff3[] = static::createRangeDifference3(
116
                            $myIterator, $yourIterator, $diff3, $right, $left, $changeRangeStart, $changeRangeEnd);
117
                        continue;
118
                    } elseif (0 === $myIterator->getDifference()->getLeftLength()) {
119
                        // Insertion into a position, and modification to the next line, is not conflict.
120
                        $startThread = $myIterator;
121
                    } elseif (0 === $yourIterator->getDifference()->getLeftLength()) {
122
                        $startThread = $yourIterator;
123
                    } else {
124
                        // Modifications to overlapping lines is conflict.
125
                        $startThread = $myIterator;
126
                    }
127
                }
128
            }
129
130
            $changeRangeStart = $startThread->getDifference()->getLeftStart();
131
            $changeRangeEnd = $startThread->getDifference()->getLeftEnd();
132
            $startThread->next();
133
134
            // Check for overlapping changes with other thread. Merge overlapping changes with this range.
135
            $other = $startThread->other($myIterator, $yourIterator);
136
137
            while (null !== $other->getDifference() && $other->getDifference()->getLeftStart() < $changeRangeEnd) {
138
                $newMax = $other->getDifference()->getLeftEnd();
139
                $other->next();
140
141
                if ($newMax > $changeRangeEnd) {
142
                    $changeRangeEnd = $newMax;
143
                    $other = $other->other($myIterator, $yourIterator);
144
                }
145
            }
146
147
            $diff3[] = static::createRangeDifference3(
148
                $myIterator, $yourIterator, $diff3, $right, $left, $changeRangeStart, $changeRangeEnd);
149
        }
150
151
        // Remove sentinel.
152
        array_shift($diff3);
153
154
        return $diff3;
155
    }
156
157
    /**
158
     * Finds the differences among two RangeComparatorInterfaces. In contrast
159
     * to findDifferences, the result contains RangeDifference elements for
160
     * non-differing ranges too.
161
     *
162
     * @param RangeComparatorInterface $left
163
     * @param RangeComparatorInterface $right
164
     * @return RangeDifference[]
165
     */
166
    public static function findRanges(RangeComparatorInterface $left, RangeComparatorInterface $right): array {
167
        $in = static::findDifferences($left, $right);
168
        $out = [];
169
170
        $mStart = 0;
171
        $yStart = 0;
172
173
        for ($i = 0; $i < count($in); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
174
            $es = $in[$i];
175
            $rd = new RangeDifference(
176
                RangeDifference::NOCHANGE,
177
                $mStart, $es->getRightStart() - $mStart,
178
                $yStart, $es->getLeftStart() - $yStart);
179
180
            if (0 !== $rd->getMaxLength()) {
181
                $out[] = $rd;
182
            }
183
184
            $out[] = $es;
185
186
            $mStart = $es->getRightEnd();
187
            $yStart = $es->getLeftEnd();
188
        }
189
190
        $rd = new RangeDifference(RangeDifference::NOCHANGE,
191
            $mStart, $right->getRangeCount() - $mStart,
192
            $yStart, $left->getRangeCount() - $yStart);
193
194
        if ($rd->getMaxLength() > 0) {
195
            $out[] = $rd;
196
        }
197
198
        return $out;
199
    }
200
201
    /**
202
     * Finds the differences among three RangeComparatorInterfaces. In contrast
203
     * to findDifferences, the result contains RangeDifference elements for
204
     * non-differing ranges too. If the ancestor range comparator is null, a
205
     * two-way comparison is performed.
206
     *
207
     * @param RangeComparatorInterface $ancestor
208
     * @param RangeComparatorInterface $left
209
     * @param RangeComparatorInterface $right
210
     * @return RangeDifference[]
211
     */
212
    public static function findRanges3(
213
        RangeComparatorInterface $ancestor,
214
        RangeComparatorInterface $left,
215
        RangeComparatorInterface $right): array
216
    {
217
        if (null === $ancestor) {
218
            return static::findRanges($left, $right);
219
        }
220
221
        $in = static::findDifferences3($ancestor, $left, $right);
222
        $out = [];
223
224
        $mStart = 0;
225
        $yStart = 0;
226
        $aStart = 0;
227
228
        for ($i = 0; $i < \count($in); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
229
            $es = $in[$i];
230
            $rd = new RangeDifference(RangeDifference::NOCHANGE,
231
                $mStart, $es->getRightStart() - $mStart,
232
                $yStart, $es->getLeftStart() - $yStart,
233
                $aStart, $es->getAncestorStart() - $aStart);
234
235
            if ($rd->getMaxLength() > 0) {
236
                $out[] = $rd;
237
            }
238
239
            $out[] = $es;
240
241
            $mStart = $es->getRightEnd();
242
            $yStart = $es->getLeftEnd();
243
            $aStart = $es->getAncestorEnd();
244
        }
245
246
        $rd = new RangeDifference(RangeDifference::NOCHANGE,
247
            $mStart, $right->getRangeCount() - $mStart,
248
            $yStart, $left->getRangeCount() - $yStart,
249
            $aStart, $ancestor->getRangeCount() - $aStart);
250
251
        if ($rd->getMaxLength() > 0) {
252
            $out[] = $rd;
253
        }
254
255
        return $out;
256
    }
257
258
    /**
259
     * @param DifferencesIterator      $myIterator
260
     * @param DifferencesIterator      $yourIterator
261
     * @param array                    $diff3
262
     * @param RangeComparatorInterface $right
263
     * @param RangeComparatorInterface $left
264
     * @param int                      $changeRightStart
265
     * @param int                      $changeRightEnd
266
     * @return RangeDifference
267
     */
268
    private static function createRangeDifference3(
269
        DifferencesIterator $myIterator,
270
        DifferencesIterator $yourIterator,
271
        array &$diff3,
272
        RangeComparatorInterface $right,
273
        RangeComparatorInterface $left,
274
        int $changeRightStart,
275
        int $changeRightEnd
276
    ): RangeDifference {
277
        $kind = RangeDifference::ERROR;
278
        /** @var RangeDifference $last */
279
        $last = $diff3[\count($diff3) - 1];
280
281
        // At least one range array must be non-empty.
282
        assert(true === ($myIterator->getCount() !== 0 || $yourIterator->getCount() !== 0));
283
284
        // Find corresponding lines to changeRangeStart/End in right and left.
285
        if ($myIterator->getCount() === 0) {
286
            // Only left changed.
287
            $rightStart = $changeRightStart - $last->getAncestorEnd() + $last->getRightEnd();
288
            $rightEnd = $changeRightEnd - $last->getAncestorEnd() + $last->getRightEnd();
289
            $kind = RangeDifference::LEFT;
290
        } else {
291
            $myRange = $myIterator->getRange();
292
            $f = $myRange[0];
293
            $l = $myRange[\count($myRange) - 1];
294
            $rightStart = $changeRightStart - $f->getLeftStart() + $f->getRightStart();
295
            $rightEnd = $changeRightEnd - $l->getLeftEnd() + $l->getRightEnd();
296
        }
297
298
        if ($yourIterator->getCount() === 0) {
299
            // Only right changed.
300
            $leftStart = $changeRightStart - $last->getAncestorEnd() + $last->getLeftEnd();
301
            $leftEnd = $changeRightEnd - $last->getAncestorEnd() + $last->getLeftEnd();
302
            $kind = RangeDifference::RIGHT;
303
        } else {
304
            $yourRange = $yourIterator->getRange();
305
            $f = $yourRange[0];
306
            $l = $yourRange[\count($yourRange) - 1];
307
            $leftStart = $changeRightStart - $f->getLeftStart() + $f->getRightStart();
308
            $leftEnd = $changeRightEnd - $l->getLeftEnd() + $l->getRightEnd();
309
        }
310
311
        if ($kind === RangeDifference::ERROR) {
312
            // Overlapping change (conflict) -> compare the changed ranges.
313
            if (static::rangeSpansEqual(
314
                $right, $rightStart, $rightEnd - $rightStart,
315
                $left, $leftStart, $leftEnd - $leftStart)) {
316
                $kind = RangeDifference::ANCESTOR;
317
            } else {
318
                $kind = RangeDifference::CONFLICT;
319
            }
320
        }
321
322
        return new RangeDifference(
323
            $kind,
324
            $rightStart, $rightEnd - $rightStart,
325
            $leftStart, $leftEnd - $leftStart,
326
            $changeRightStart, $changeRightEnd - $changeRightStart);
327
    }
328
329
    /**
330
     * Tests whether right and left changed in the same way.
331
     *
332
     * @param RangeComparatorInterface $right
333
     * @param int                      $rightStart
334
     * @param int                      $rightLength
335
     * @param RangeComparatorInterface $left
336
     * @param int                      $leftStart
337
     * @param int                      $leftLength
338
     * @return bool
339
     */
340
    private static function rangeSpansEqual(
341
        RangeComparatorInterface $right,
342
        int $rightStart,
343
        int $rightLength,
344
        RangeComparatorInterface $left,
345
        int $leftStart,
346
        int $leftLength
347
    ): bool {
348
        if ($rightLength === $leftLength) {
349
            for ($i = 0; $i < $rightLength; $i++) {
350
                if (!static::rangesEqual(
351
                    $right, $rightStart + $i,
352
                    $left, $leftStart + $i)) {
353
                    break;
354
                }
355
            }
356
357
            if ($i === $rightLength) {
358
                return true;
359
            }
360
        }
361
362
        return false;
363
    }
364
365
    /**
366
     * Tests if two ranges are equal.
367
     *
368
     * @param RangeComparatorInterface $a
369
     * @param int                      $ai
370
     * @param RangeComparatorInterface $b
371
     * @param int                      $bi
372
     * @return bool
373
     */
374
    private static function rangesEqual(
375
        RangeComparatorInterface $a,
376
        int $ai,
377
        RangeComparatorInterface $b,
378
        int $bi
379
    ): bool {
380
        return $a->rangesEqual($ai, $b, $bi);
381
    }
382
}
383