Skip to content

Commit 8bb4a9a

Browse files
committed
first implementation of meyers diff with linear space
1 parent 3d5343c commit 8bb4a9a

File tree

9 files changed

+514
-4
lines changed

9 files changed

+514
-4
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
* Copyright 2021 java-diff-utils.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.difflib.algorithm.myers;
17+
18+
import com.github.difflib.algorithm.Change;
19+
import com.github.difflib.algorithm.DiffAlgorithmI;
20+
import com.github.difflib.algorithm.DiffAlgorithmListener;
21+
import com.github.difflib.patch.DeltaType;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import java.util.function.BiPredicate;
26+
27+
/**
28+
*
29+
* @author tw
30+
*/
31+
public class MeyersDiffWithLinearSpace<T> implements DiffAlgorithmI<T> {
32+
33+
private final BiPredicate<T, T> equalizer;
34+
35+
public MeyersDiffWithLinearSpace() {
36+
equalizer = Object::equals;
37+
}
38+
39+
public MeyersDiffWithLinearSpace(final BiPredicate<T, T> equalizer) {
40+
Objects.requireNonNull(equalizer, "equalizer must not be null");
41+
this.equalizer = equalizer;
42+
}
43+
44+
@Override
45+
public List<Change> computeDiff(List<T> source, List<T> target, DiffAlgorithmListener progress) {
46+
DiffData data = new DiffData(source, target);
47+
//shouldn't it be source.size() - 1?
48+
buildScript(data, 0, source.size(), 0, target.size());
49+
return data.script;
50+
}
51+
52+
private void buildScript(DiffData data, int start1, int end1, int start2, int end2) {
53+
final Snake middle = getMiddleSnake(data, start1, end1, start2, end2);
54+
if (middle == null
55+
|| middle.start == end1 && middle.diag == end1 - end2
56+
|| middle.end == start1 && middle.diag == start1 - start2) {
57+
int i = start1;
58+
int j = start2;
59+
while (i < end1 || j < end2) {
60+
if (i < end1 && j < end2 && equalizer.test(data.source.get(i), data.target.get(j))) {
61+
//script.append(new KeepCommand<>(left.charAt(i)));
62+
++i;
63+
++j;
64+
} else {
65+
//TODO: compress these commands.
66+
if (end1 - start1 > end2 - start2) {
67+
//script.append(new DeleteCommand<>(left.charAt(i)));
68+
data.script.add(new Change(DeltaType.DELETE, i, i + 1, j, j));
69+
++i;
70+
} else {
71+
//script.append(new InsertCommand<>(right.charAt(j)));
72+
data.script.add(new Change(DeltaType.INSERT, i, i, j, j + 1));
73+
++j;
74+
}
75+
}
76+
}
77+
} else {
78+
buildScript(data, start1, middle.start, start2, middle.start - middle.diag);
79+
// for (int i = middle.getStart(); i < middle.getEnd(); ++i) {
80+
// script.append(new KeepCommand<>(left.charAt(i)));
81+
// }
82+
buildScript(data, middle.end, end1, middle.end - middle.diag, end2);
83+
}
84+
}
85+
86+
private Snake getMiddleSnake(DiffData data, int start1, int end1, int start2, int end2) {
87+
final int m = end1 - start1;
88+
final int n = end2 - start2;
89+
if (m == 0 || n == 0) {
90+
return null;
91+
}
92+
93+
final int delta = m - n;
94+
final int sum = n + m;
95+
final int offset = (sum % 2 == 0 ? sum : sum + 1) / 2;
96+
data.vDown[1 + offset] = start1;
97+
data.vUp[1 + offset] = end1 + 1;
98+
99+
for (int d = 0; d <= offset; ++d) {
100+
// Down
101+
for (int k = -d; k <= d; k += 2) {
102+
// First step
103+
104+
final int i = k + offset;
105+
if (k == -d || k != d && data.vDown[i - 1] < data.vDown[i + 1]) {
106+
data.vDown[i] = data.vDown[i + 1];
107+
} else {
108+
data.vDown[i] = data.vDown[i - 1] + 1;
109+
}
110+
111+
int x = data.vDown[i];
112+
int y = x - start1 + start2 - k;
113+
114+
while (x < end1 && y < end2 && equalizer.test(data.source.get(x), data.target.get(y))) {
115+
data.vDown[i] = ++x;
116+
++y;
117+
}
118+
// Second step
119+
if (delta % 2 != 0 && delta - d <= k && k <= delta + d) {
120+
if (data.vUp[i - delta] <= data.vDown[i]) {
121+
return buildSnake(data, data.vUp[i - delta], k + start1 - start2, end1, end2);
122+
}
123+
}
124+
}
125+
126+
// Up
127+
for (int k = delta - d; k <= delta + d; k += 2) {
128+
// First step
129+
final int i = k + offset - delta;
130+
if (k == delta - d
131+
|| k != delta + d && data.vUp[i + 1] <= data.vUp[i - 1]) {
132+
data.vUp[i] = data.vUp[i + 1] - 1;
133+
} else {
134+
data.vUp[i] = data.vUp[i - 1];
135+
}
136+
137+
int x = data.vUp[i] - 1;
138+
int y = x - start1 + start2 - k;
139+
while (x >= start1 && y >= start2 && equalizer.test(data.source.get(x), data.target.get(y))) {
140+
data.vUp[i] = x--;
141+
y--;
142+
}
143+
// Second step
144+
if (delta % 2 == 0 && -d <= k && k <= d) {
145+
if (data.vUp[i] <= data.vDown[i + delta]) {
146+
return buildSnake(data, data.vUp[i], k + start1 - start2, end1, end2);
147+
}
148+
}
149+
}
150+
}
151+
152+
// According to Myers, this cannot happen
153+
throw new IllegalStateException("could not find a diff path");
154+
}
155+
156+
private Snake buildSnake(DiffData data, final int start, final int diag, final int end1, final int end2) {
157+
int end = start;
158+
while (end - diag < end2 && end < end1 && equalizer.test(data.source.get(end), data.target.get(end - diag))) {
159+
++end;
160+
}
161+
return new Snake(start, end, diag);
162+
}
163+
164+
class DiffData {
165+
166+
final int size;
167+
final int[] vDown;
168+
final int[] vUp;
169+
final List<Change> script;
170+
final List<T> source;
171+
final List<T> target;
172+
173+
public DiffData(List<T> source, List<T> target) {
174+
this.source = source;
175+
this.target = target;
176+
size = source.size() + target.size() + 2;
177+
vDown = new int[size];
178+
vUp = new int[size];
179+
script = new ArrayList<>();
180+
}
181+
}
182+
183+
class Snake {
184+
185+
final int start;
186+
final int end;
187+
final int diag;
188+
189+
public Snake(final int start, final int end, final int diag) {
190+
this.start = start;
191+
this.end = end;
192+
this.diag = diag;
193+
}
194+
}
195+
}

java-diff-utils/src/main/java/com/github/difflib/algorithm/myers/MyersDiff.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@
3030
*/
3131
public final class MyersDiff<T> implements DiffAlgorithmI<T> {
3232

33-
private final BiPredicate<T, T> DEFAULT_EQUALIZER = Object::equals;
3433
private final BiPredicate<T, T> equalizer;
3534

3635
public MyersDiff() {
37-
equalizer = DEFAULT_EQUALIZER;
36+
equalizer = Object::equals;
3837
}
3938

4039
public MyersDiff(final BiPredicate<T, T> equalizer) {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2021 java-diff-utils.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.difflib.algorithm.myers;
17+
18+
import com.github.difflib.algorithm.DiffAlgorithmListener;
19+
import com.github.difflib.patch.Patch;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import org.junit.jupiter.api.Test;
24+
import static org.junit.jupiter.api.Assertions.*;
25+
26+
/**
27+
*
28+
* @author tw
29+
*/
30+
public class MeyersDiffWithLinearSpaceTest {
31+
32+
@Test
33+
public void testDiffMyersExample1Forward() {
34+
List<String> original = Arrays.asList("A", "B", "C", "A", "B", "B", "A");
35+
List<String> revised = Arrays.asList("C", "B", "A", "B", "A", "C");
36+
final Patch<String> patch = Patch.generate(original, revised, new MeyersDiffWithLinearSpace<String>().computeDiff(original, revised, null));
37+
assertNotNull(patch);
38+
System.out.println(patch);
39+
assertEquals(4, patch.getDeltas().size());
40+
assertEquals("Patch{deltas=[[DeleteDelta, position: 0, lines: [A, B]], [InsertDelta, position: 3, lines: [B]], [DeleteDelta, position: 5, lines: [B]], [InsertDelta, position: 7, lines: [C]]]}", patch.toString());
41+
}
42+
43+
@Test
44+
public void testDiffMyersExample1ForwardWithListener() {
45+
List<String> original = Arrays.asList("A", "B", "C", "A", "B", "B", "A");
46+
List<String> revised = Arrays.asList("C", "B", "A", "B", "A", "C");
47+
48+
List<String> logdata = new ArrayList<>();
49+
final Patch<String> patch = Patch.generate(original, revised,
50+
new MeyersDiffWithLinearSpace<String>().computeDiff(original, revised, new DiffAlgorithmListener() {
51+
@Override
52+
public void diffStart() {
53+
logdata.add("start");
54+
}
55+
56+
@Override
57+
public void diffStep(int value, int max) {
58+
logdata.add(value + " - " + max);
59+
}
60+
61+
@Override
62+
public void diffEnd() {
63+
logdata.add("end");
64+
}
65+
}));
66+
assertNotNull(patch);
67+
System.out.println(patch);
68+
assertEquals(4, patch.getDeltas().size());
69+
assertEquals("Patch{deltas=[[DeleteDelta, position: 0, lines: [A, B]], [InsertDelta, position: 3, lines: [B]], [DeleteDelta, position: 5, lines: [B]], [InsertDelta, position: 7, lines: [C]]]}", patch.toString());
70+
System.out.println(logdata);
71+
assertEquals(8, logdata.size());
72+
}
73+
}

java-diff-utils/src/test/java/com/github/difflib/algorithm/myers/MyersDiffTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,4 @@ public void diffEnd() {
6969
System.out.println(logdata);
7070
assertEquals(8, logdata.size());
7171
}
72-
7372
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.github.difflib.algorithm.myers;
2+
3+
import com.github.difflib.patch.*;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.fail;
6+
7+
import java.io.ByteArrayInputStream;
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.IOException;
10+
import java.io.ObjectInputStream;
11+
import java.io.ObjectOutputStream;
12+
import java.util.Arrays;
13+
import java.util.List;
14+
15+
import org.junit.jupiter.api.Test;
16+
17+
import com.github.difflib.DiffUtils;
18+
19+
public class WithMeyersDiffWithLinearSpacePatchTest {
20+
21+
@Test
22+
public void testPatch_Insert() {
23+
final List<String> insertTest_from = Arrays.asList("hhh");
24+
final List<String> insertTest_to = Arrays.asList("hhh", "jjj", "kkk", "lll");
25+
26+
final Patch<String> patch = DiffUtils.diff(insertTest_from, insertTest_to, new MeyersDiffWithLinearSpace<String>());
27+
try {
28+
assertEquals(insertTest_to, DiffUtils.patch(insertTest_from, patch));
29+
} catch (PatchFailedException e) {
30+
fail(e.getMessage());
31+
}
32+
}
33+
34+
@Test
35+
public void testPatch_Delete() {
36+
final List<String> deleteTest_from = Arrays.asList("ddd", "fff", "ggg", "hhh");
37+
final List<String> deleteTest_to = Arrays.asList("ggg");
38+
39+
final Patch<String> patch = DiffUtils.diff(deleteTest_from, deleteTest_to, new MeyersDiffWithLinearSpace<String>());
40+
try {
41+
assertEquals(deleteTest_to, DiffUtils.patch(deleteTest_from, patch));
42+
} catch (PatchFailedException e) {
43+
fail(e.getMessage());
44+
}
45+
}
46+
47+
@Test
48+
public void testPatch_Change() {
49+
final List<String> changeTest_from = Arrays.asList("aaa", "bbb", "ccc", "ddd");
50+
final List<String> changeTest_to = Arrays.asList("aaa", "bxb", "cxc", "ddd");
51+
52+
final Patch<String> patch = DiffUtils.diff(changeTest_from, changeTest_to, new MeyersDiffWithLinearSpace<String>());
53+
try {
54+
assertEquals(changeTest_to, DiffUtils.patch(changeTest_from, patch));
55+
} catch (PatchFailedException e) {
56+
fail(e.getMessage());
57+
}
58+
}
59+
60+
@Test
61+
public void testPatch_Serializable() throws IOException, ClassNotFoundException {
62+
final List<String> changeTest_from = Arrays.asList("aaa", "bbb", "ccc", "ddd");
63+
final List<String> changeTest_to = Arrays.asList("aaa", "bxb", "cxc", "ddd");
64+
65+
final Patch<String> patch = DiffUtils.diff(changeTest_from, changeTest_to, new MeyersDiffWithLinearSpace<String>());
66+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
67+
ObjectOutputStream out = new ObjectOutputStream(baos);
68+
out.writeObject(patch);
69+
out.close();
70+
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
71+
ObjectInputStream in = new ObjectInputStream(bais);
72+
Patch<String> result = (Patch<String>) in.readObject();
73+
in.close();
74+
75+
try {
76+
assertEquals(changeTest_to, DiffUtils.patch(changeTest_from, result));
77+
} catch (PatchFailedException e) {
78+
fail(e.getMessage());
79+
}
80+
81+
}
82+
83+
@Test
84+
public void testPatch_Change_withExceptionProcessor() {
85+
final List<String> changeTest_from = Arrays.asList("aaa", "bbb", "ccc", "ddd");
86+
final List<String> changeTest_to = Arrays.asList("aaa", "bxb", "cxc", "ddd");
87+
88+
final Patch<String> patch = DiffUtils.diff(changeTest_from, changeTest_to, new MeyersDiffWithLinearSpace<String>());
89+
90+
changeTest_from.set(2, "CDC");
91+
92+
patch.withConflictOutput(Patch.CONFLICT_PRODUCES_MERGE_CONFLICT);
93+
94+
try {
95+
List<String> data = DiffUtils.patch(changeTest_from, patch);
96+
assertEquals(9, data.size());
97+
98+
assertEquals(Arrays.asList("aaa", "<<<<<< HEAD", "bbb", "CDC", "======", "bbb", "ccc", ">>>>>>> PATCH", "ddd"), data);
99+
100+
} catch (PatchFailedException e) {
101+
fail(e.getMessage());
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)