RangeComparatorLCS::getDifferences()   D
last analyzed

Complexity

Conditions 20
Paths 66

Size

Total Lines 82
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 20.2173

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 20
eloc 49
c 1
b 0
f 0
nc 66
nop 0
dl 0
loc 82
ccs 45
cts 49
cp 0.9184
crap 20.2173
rs 4.1666

How to fix   Long Method    Complexity   

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
use SN\RangeDifferencer\Core\LCS;
14
15
/**
16
 * RangeComparator using Longest Common Subsequence.
17
 *
18
 * @internal
19
 */
20
class RangeComparatorLCS extends LCS
21
{
22
    /** @var RangeComparatorInterface */
23
    private $comparator1;
24
25
    /** @var RangeComparatorInterface */
26
    private $comparator2;
27
28
    /** @var int[][] */
29
    private $lcs;
30
31
    /**
32
     * Default values.
33
     *
34
     * @param RangeComparatorInterface $comparator1
35
     * @param RangeComparatorInterface $comparator2
36
     */
37 30
    public function __construct(RangeComparatorInterface $comparator1, RangeComparatorInterface $comparator2)
38
    {
39 30
        $this->comparator1 = $comparator1;
40 30
        $this->comparator2 = $comparator2;
41 30
    }
42
43
    /**
44
     * @param RangeComparatorInterface $left
45
     * @param RangeComparatorInterface $right
46
     * @return RangeDifference[]
47
     */
48 17
    public static function findDifferences(RangeComparatorInterface $left, RangeComparatorInterface $right): array
49
    {
50 17
        $lcs = new static($left, $right);
51 17
        $lcs->longestCommonSubsequence();
52
53 17
        return $lcs->getDifferences();
54
    }
55
56
    /**
57
     * @return int
58
     */
59 21
    protected function getLength1(): int
60
    {
61 21
        return $this->comparator1->getRangeCount();
62
    }
63
64
    /**
65
     * @return int
66
     */
67 21
    protected function getLength2(): int
68
    {
69 21
        return $this->comparator2->getRangeCount();
70
    }
71
72
    /**
73
     * @param int $lcsLength
74
     */
75 19
    protected function initializeLcs(int $lcsLength): void
76
    {
77 19
        $this->lcs = \array_fill(0, 2, \array_fill(0, $lcsLength, 0));
78 19
    }
79
80
    /**
81
     * @param int $i1
82
     * @param int $i2
83
     * @return bool
84
     */
85 18
    protected function isRangeEqual(int $i1, int $i2): bool
86
    {
87 18
        return $this->comparator1->rangesEqual($i1, $this->comparator2, $i2);
88
    }
89
90
    /**
91
     * @param int $sl1
92
     * @param int $sl2
93
     */
94 17
    protected function setLcs(int $sl1, int $sl2): void
95
    {
96 17
        $this->lcs[0][$sl1] = $sl1 + 1;
97 17
        $this->lcs[1][$sl1] = $sl2 + 1;
98 17
    }
99
100
    /**
101
     * @return RangeDifference[]
102
     */
103 23
    public function getDifferences(): array
104
    {
105 23
        $differences = [];
106 23
        $length = $this->getLength();
107
108 23
        if (0 === $length) {
109 6
            $differences[] = new RangeDifference(
110 6
                RangeDifference::CHANGE,
111 6
                0, $this->comparator2->getRangeCount(),
112 6
                0, $this->comparator1->getRangeCount());
113
        } else {
114 17
            $index1 = 0;
115 17
            $index2 = 0;
116 17
            $s1 = -1;
117 17
            $s2 = -1;
118
119 17
            while ($index1 < \count($this->lcs[0]) && $index2 < \count($this->lcs[1])) {
120
                // Move both LCS lists to the next occupied slot.
121 17
                while (0 === $l1 = $this->lcs[0][$index1]) {
122 7
                    $index1++;
123
124 7
                    if ($index1 >= \count($this->lcs[0])) {
125 7
                        break;
126
                    }
127
                }
128
129 17
                if ($index1 >= \count($this->lcs[0])) {
130 7
                    break;
131
                }
132
133 17
                while (0 === $l2 = $this->lcs[1][$index2]) {
134
                    $index2++;
135
136
                    if ($index2 >= \count($this->lcs[1])) {
137
                        break;
138
                    }
139
                }
140
141 17
                if ($index2 >= \count($this->lcs[1])) {
142
                    break;
143
                }
144
145
                // Convert the entry to an array index (see setLcs(int, int)).
146 17
                $end1 = $l1 - 1;
147 17
                $end2 = $l2 - 1;
148
149 17
                if (-1 === $s1 && (0 !== $end1 || 0 !== $end2)) {
150
                    // There is a diff at the beginning.
151
                    // TODO: We need to confirm that this is the proper order.
152 2
                    $differences[] = new RangeDifference(RangeDifference::CHANGE, 0, $end2, 0, $end1);
153 17
                } elseif ($end1 !== $s1 + 1 || $end2 !== $s2 + 1) {
154
                    // A diff was found on one of the sides.
155 12
                    $leftStart = $s1 + 1;
156 12
                    $leftLength = $end1 - $leftStart;
157 12
                    $rightStart = $s2 + 1;
158 12
                    $rightLength = $end2 - $rightStart;
159
160
                    // TODO: We need to confirm that this is the proper order.
161 12
                    $differences[] = new RangeDifference(
162 12
                        RangeDifference::CHANGE, $rightStart, $rightLength, $leftStart, $leftLength);
163
                }
164
165 17
                $s1 = $end1;
166 17
                $s2 = $end2;
167 17
                $index1++;
168 17
                $index2++;
169
            }
170
171 17
            if (-1 !== $s1 && ($s1 + 1 < $this->comparator1->getRangeCount() ||
172 17
                    $s2 + 1 < $this->comparator2->getRangeCount())) {
173 2
                $leftStart = $s1 < $this->comparator1->getRangeCount() ? $s1 + 1 : $s1;
174 2
                $rightStart = $s2 < $this->comparator2->getRangeCount() ? $s2 + 1 : $s2;
175
176
                // TODO: We need to confirm that this is the proper order.
177 2
                $differences[] = new RangeDifference(
178 2
                    RangeDifference::CHANGE,
179 2
                    $rightStart, $this->comparator2->getRangeCount() - $s2 + 1,
180 2
                    $leftStart, $this->comparator1->getRangeCount() - $s1 + 1);
181
            }
182
        }
183
184 23
        return $differences;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190 17
    public function longestCommonSubsequence(): void
191
    {
192 17
        parent::longestCommonSubsequence();
193
194
        // The LCS can be null if one of the sides is empty.
195 17
        if (null !== $this->lcs) {
196 17
            $this->compactAndShiftLCS($this->lcs[0], $this->getLength(), $this->comparator1);
197 17
            $this->compactAndShiftLCS($this->lcs[1], $this->getLength(), $this->comparator2);
198
        }
199 17
    }
200
201
    /**
202
     * This method takes an LCS result interspersed with zeros (i.e. empty slots  from the LCS algorithm), compacts it
203
     * and shifts the LCS chunks as far towards the front as possible. This tends to produce good results most of the
204
     * time.
205
     *
206
     * @param int[]                    $lcsSide
207
     * @param int                      $length
208
     * @param RangeComparatorInterface $comparator
209
     */
210 17
    private function compactAndShiftLCS(array &$lcsSide, int $length, RangeComparatorInterface $comparator): void
211
    {
212
        // If the LCS is empty, just return.
213 17
        if (0 === $length) {
214
            return;
215
        }
216
217
        // Skip any leading slots.
218 17
        $j = 0;
219
220 17
        while (0 === $lcsSide[$j]) {
221 1
            $j++;
222
        }
223
224
        // Put the first non-empty value in position 0.
225 17
        $lcsSide[0] = $lcsSide[$j];
226 17
        $j++;
227
228
        // Push all non-empty values down into the first N slots (where N is the length)
229 17
        for ($i = 1; $i < $length; $i++) {
230 17
            while (0 === $lcsSide[$j]) {
231 5
                $j++;
232
            }
233
234
            /*
235
             * Push the difference down as far as possible by comparing the line at the start of the diff with the line
236
             * and the end and adjusting if they are the same.
237
             */
238 17
            $nextLine = $lcsSide[$i - 1] + 1;
239
240 17
            if ($nextLine !== $lcsSide[$j] && $comparator->rangesEqual($nextLine - 1, $comparator, $lcsSide[$j] - 1)) {
241
                $lcsSide[$i] = $nextLine;
242
            } else {
243 17
                $lcsSide[$i] = $lcsSide[$j];
244
            }
245
246 17
            $j++;
247
        }
248
249
        // Zero all slots after the length.
250 17
        for ($i = $length, $iMax = \count($lcsSide); $i < $iMax; $i++) {
251 7
            $lcsSide[$i] = 0;
252
        }
253 17
    }
254
}
255