Passed
Push — master ( 9bcdfc...42324d )
by Steve
45s
created

RangeComparatorLCS::compactAndShiftLCS()   B

Complexity

Conditions 8
Paths 21

Size

Total Lines 42
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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