RangeDifferencer   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Test Coverage

Coverage 66.67%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 142
c 1
b 0
f 0
dl 0
loc 340
ccs 96
cts 144
cp 0.6667
rs 9.28
wmc 39

8 Methods

Rating   Name   Duplication   Size   Complexity  
A rangesEqual() 0 7 1
A findRanges() 0 34 4
A rangeSpansEqual() 0 21 5
A __construct() 0 2 1
A findDifferences() 0 3 1
C findDifferences3() 0 89 17
A findRanges3() 0 40 4
B createRangeDifference3() 0 59 6
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