Skip to content

Commit db20b12

Browse files
committed
feat: introduce MyersDiffWithLinearSpace
1 parent 0a6fbf7 commit db20b12

File tree

9 files changed

+566
-1471
lines changed

9 files changed

+566
-1471
lines changed

build.gradle.kts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.gitlab.arturbosch.detekt.Detekt
66
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
77
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
88
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
9+
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
910

1011
plugins {
1112
kotlin("multiplatform")
@@ -47,8 +48,20 @@ kotlin {
4748
}
4849

4950
js {
50-
browser()
51-
nodejs()
51+
val testConfig: (KotlinJsTest).() -> Unit = {
52+
useMocha {
53+
// Override default 2s timeout
54+
timeout = "120s"
55+
}
56+
}
57+
58+
browser {
59+
testTask(testConfig)
60+
}
61+
62+
nodejs {
63+
testTask(testConfig)
64+
}
5265
}
5366

5467
linuxX64()

kotlin-js-store/yarn.lock

Lines changed: 8 additions & 1354 deletions
Large diffs are not rendered by default.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright 2024 Peter Trifanov.
3+
* Copyright 2009-2021 java-diff-utils.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
* This file has been modified by Peter Trifanov when porting from Java to Kotlin.
18+
*/
19+
package io.github.petertrr.diffutils.algorithm.myers
20+
21+
import io.github.petertrr.diffutils.algorithm.Change
22+
import io.github.petertrr.diffutils.algorithm.DiffAlgorithm
23+
import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener
24+
import io.github.petertrr.diffutils.algorithm.DiffEqualizer
25+
import io.github.petertrr.diffutils.algorithm.EqualsDiffEqualizer
26+
import io.github.petertrr.diffutils.patch.DeltaType
27+
28+
public class MyersDiffWithLinearSpace<T>(
29+
private val equalizer: DiffEqualizer<T> = EqualsDiffEqualizer(),
30+
) : DiffAlgorithm<T> {
31+
public override fun computeDiff(source: List<T>, target: List<T>, progress: DiffAlgorithmListener): List<Change> {
32+
progress.diffStart()
33+
34+
val data = DiffData(source, target)
35+
val maxIdx = source.size + target.size
36+
val progressWrapper = DelegateAlgorithmListener(maxIdx, progress)
37+
buildScript(data, 0, source.size, 0, target.size, progressWrapper)
38+
39+
progress.diffEnd()
40+
return data.script
41+
}
42+
43+
private fun buildScript(
44+
data: DiffData<T>,
45+
start1: Int,
46+
end1: Int,
47+
start2: Int,
48+
end2: Int,
49+
progress: DiffAlgorithmListener,
50+
) {
51+
progress.diffStep((end1 - start1) / 2 + (end2 - start2) / 2, -1)
52+
53+
val middle = getMiddleSnake(data, start1, end1, start2, end2)
54+
55+
if (middle == null ||
56+
middle.start == end1 && middle.diag == end1 - end2 ||
57+
middle.end == start1 && middle.diag == start1 - start2
58+
) {
59+
var i = start1
60+
var j = start2
61+
62+
while (i < end1 || j < end2) {
63+
if (i < end1 && j < end2 && equalizer.test(data.source[i], data.target[j])) {
64+
// script.append(new KeepCommand<>(left.charAt(i)));
65+
++i
66+
++j
67+
} else {
68+
// TODO: compress these commands
69+
if (end1 - start1 > end2 - start2) {
70+
// script.append(new DeleteCommand<>(left.charAt(i)));
71+
if (data.script.isEmpty() ||
72+
data.script[data.script.size - 1].endOriginal != i ||
73+
data.script[data.script.size - 1].deltaType != DeltaType.DELETE
74+
) {
75+
data.script.add(Change(DeltaType.DELETE, i, i + 1, j, j))
76+
} else {
77+
data.script[data.script.size - 1] =
78+
data.script[data.script.size - 1].copy(endOriginal = i + 1)
79+
}
80+
81+
++i
82+
} else {
83+
if (data.script.isEmpty() ||
84+
data.script[data.script.size - 1].endRevised != j ||
85+
data.script[data.script.size - 1].deltaType != DeltaType.INSERT
86+
) {
87+
data.script.add(Change(DeltaType.INSERT, i, i, j, j + 1))
88+
} else {
89+
data.script[data.script.size - 1] =
90+
data.script[data.script.size - 1].copy(endRevised = j + 1)
91+
}
92+
93+
++j
94+
}
95+
}
96+
}
97+
} else {
98+
buildScript(data, start1, middle.start, start2, middle.start - middle.diag, progress)
99+
buildScript(data, middle.end, end1, middle.end - middle.diag, end2, progress)
100+
}
101+
}
102+
103+
private fun getMiddleSnake(data: DiffData<T>, start1: Int, end1: Int, start2: Int, end2: Int): Snake? {
104+
val m = end1 - start1
105+
val n = end2 - start2
106+
107+
if (m == 0 || n == 0) {
108+
return null
109+
}
110+
111+
val delta = m - n
112+
val sum = n + m
113+
val offset = (if (sum % 2 == 0) sum else sum + 1) / 2
114+
data.vDown[1 + offset] = start1
115+
data.vUp[1 + offset] = end1 + 1
116+
117+
for (d in 0..offset) {
118+
// Down
119+
var k = -d
120+
121+
while (k <= d) {
122+
// First step
123+
val i = k + offset
124+
125+
if (k == -d || k != d && data.vDown[i - 1] < data.vDown[i + 1]) {
126+
data.vDown[i] = data.vDown[i + 1]
127+
} else {
128+
data.vDown[i] = data.vDown[i - 1] + 1
129+
}
130+
131+
var x = data.vDown[i]
132+
var y = x - start1 + start2 - k
133+
134+
while (x < end1 && y < end2 && equalizer.test(data.source[x], data.target[y])) {
135+
data.vDown[i] = ++x
136+
++y
137+
}
138+
139+
// Second step
140+
if (delta % 2 != 0 && delta - d <= k && k <= delta + d) {
141+
if (data.vUp[i - delta] <= data.vDown[i]) {
142+
return buildSnake(data, data.vUp[i - delta], k + start1 - start2, end1, end2)
143+
}
144+
}
145+
146+
k += 2
147+
}
148+
149+
// Up
150+
k = delta - d
151+
152+
while (k <= delta + d) {
153+
// First step
154+
val i = k + offset - delta
155+
156+
if (k == delta - d || k != delta + d && data.vUp[i + 1] <= data.vUp[i - 1]) {
157+
data.vUp[i] = data.vUp[i + 1] - 1
158+
} else {
159+
data.vUp[i] = data.vUp[i - 1]
160+
}
161+
162+
var x = data.vUp[i] - 1
163+
var y = x - start1 + start2 - k
164+
165+
while (x >= start1 && y >= start2 && equalizer.test(data.source[x], data.target[y])) {
166+
data.vUp[i] = x--
167+
y--
168+
}
169+
170+
// Second step
171+
if (delta % 2 == 0 && -d <= k && k <= d) {
172+
if (data.vUp[i] <= data.vDown[i + delta]) {
173+
return buildSnake(data, data.vUp[i], k + start1 - start2, end1, end2)
174+
}
175+
}
176+
177+
k += 2
178+
}
179+
}
180+
181+
// According to Myers, this cannot happen
182+
error("Could not find a diff path")
183+
}
184+
185+
private fun buildSnake(data: DiffData<T>, start: Int, diag: Int, end1: Int, end2: Int): Snake {
186+
var end = start
187+
188+
while (end - diag < end2 && end < end1 && equalizer.test(data.source[end], data.target[end - diag])) {
189+
++end
190+
}
191+
192+
return Snake(start, end, diag)
193+
}
194+
195+
private class DelegateAlgorithmListener(
196+
val maxIdx: Int,
197+
val delegate: DiffAlgorithmListener,
198+
) : DiffAlgorithmListener by delegate {
199+
override fun diffStep(value: Int, max: Int) {
200+
delegate.diffStep(value, maxIdx)
201+
}
202+
}
203+
204+
private class DiffData<T>(
205+
val source: List<T>,
206+
val target: List<T>,
207+
) {
208+
val size = source.size + target.size + 2
209+
val vDown = IntArray(size)
210+
val vUp = IntArray(size)
211+
val script = ArrayList<Change>()
212+
}
213+
214+
private class Snake(
215+
val start: Int,
216+
val end: Int,
217+
val diag: Int,
218+
)
219+
}

src/commonTest/kotlin/io/github/petertrr/diffutils/algorithm/myers/MyersDiffTest.kt

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,54 +20,62 @@ package io.github.petertrr.diffutils.algorithm.myers
2020

2121
import io.github.petertrr.diffutils.algorithm.DiffAlgorithmListener
2222
import io.github.petertrr.diffutils.patch.Patch
23-
import io.github.petertrr.diffutils.utils.deleteDeltaOf
24-
import io.github.petertrr.diffutils.utils.insertDeltaOf
2523
import kotlin.test.Test
2624
import kotlin.test.assertEquals
27-
import kotlin.test.assertNotNull
2825

2926
class MyersDiffTest {
3027
@Test
3128
fun testDiffMyersExample1Forward() {
32-
val original: List<String> = listOf("A", "B", "C", "A", "B", "B", "A")
33-
val revised: List<String> = listOf("C", "B", "A", "B", "A", "C")
34-
val patch: Patch<String> = Patch.generate(original, revised, MyersDiff<String>().computeDiff(original, revised))
35-
assertNotNull(patch)
29+
val original = listOf("A", "B", "C", "A", "B", "B", "A")
30+
val revised = listOf("C", "B", "A", "B", "A", "C")
31+
val patch = Patch.generate(original, revised, MyersDiff<String>().computeDiff(original, revised))
32+
3633
assertEquals(4, patch.deltas.size)
3734
assertEquals(
38-
"Patch{deltas=[${deleteDeltaOf(0, listOf("A", "B"))}, ${insertDeltaOf(3, 1, listOf("B"))}, ${deleteDeltaOf(5, listOf("B"), 4)}, ${insertDeltaOf(7, 5, listOf("C"))}]}",
39-
patch.toString()
35+
"Patch{deltas=[" +
36+
"[DeleteDelta, position: 0, lines: [A, B]], " +
37+
"[InsertDelta, position: 3, lines: [B]], " +
38+
"[DeleteDelta, position: 5, lines: [B]], " +
39+
"[InsertDelta, position: 7, lines: [C]]" +
40+
"]}",
41+
patch.toString(),
4042
)
4143
}
4244

4345
@Test
4446
fun testDiffMyersExample1ForwardWithListener() {
45-
val original: List<String> = listOf("A", "B", "C", "A", "B", "B", "A")
46-
val revised: List<String> = listOf("C", "B", "A", "B", "A", "C")
47-
val logdata: MutableList<String> = ArrayList()
48-
val patch: Patch<String> = Patch.generate(original, revised,
47+
val original = listOf("A", "B", "C", "A", "B", "B", "A")
48+
val revised = listOf("C", "B", "A", "B", "A", "C")
49+
val logData = ArrayList<String>()
50+
val patch = Patch.generate(
51+
original, revised,
4952
MyersDiff<String>().computeDiff(original, revised, object : DiffAlgorithmListener {
5053
override fun diffStart() {
51-
logdata.add("start")
54+
logData.add("start")
5255
}
5356

5457
override fun diffStep(value: Int, max: Int) {
55-
logdata.add("$value - $max")
58+
logData.add("$value - $max")
5659
}
5760

5861
override fun diffEnd() {
59-
logdata.add("end")
62+
logData.add("end")
6063
}
6164
})
6265
)
63-
assertNotNull(patch)
66+
6467
assertEquals(4, patch.deltas.size)
6568
assertEquals(
66-
"Patch{deltas=[${deleteDeltaOf(0, listOf("A", "B"))}, ${insertDeltaOf(3, 1, listOf("B"))}, " +
67-
"${deleteDeltaOf(5, listOf("B"), 4)}, ${insertDeltaOf(7, 5, listOf("C"))}]}",
68-
patch.toString()
69+
"Patch{deltas=[" +
70+
"[DeleteDelta, position: 0, lines: [A, B]], " +
71+
"[InsertDelta, position: 3, lines: [B]], " +
72+
"[DeleteDelta, position: 5, lines: [B]], " +
73+
"[InsertDelta, position: 7, lines: [C]]" +
74+
"]}",
75+
patch.toString(),
6976
)
70-
println(logdata)
71-
assertEquals(8, logdata.size)
77+
78+
println(logData)
79+
assertEquals(8, logData.size)
7280
}
7381
}

0 commit comments

Comments
 (0)