diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffGenerator.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffGenerator.kt index 43b97ea..2900995 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffGenerator.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffGenerator.kt @@ -10,6 +10,7 @@ import dev.andrewbailey.diff.impl.MyersDiffAlgorithm import dev.andrewbailey.diff.impl.MyersDiffOperation.Delete import dev.andrewbailey.diff.impl.MyersDiffOperation.Insert import dev.andrewbailey.diff.impl.MyersDiffOperation.Skip +import dev.andrewbailey.diff.impl.fastForEach internal object DiffGenerator { @@ -18,14 +19,12 @@ internal object DiffGenerator { updated: List, detectMoves: Boolean ): DiffResult { - val diff = MyersDiffAlgorithm(original, updated) - .generateDiff() + val diff = MyersDiffAlgorithm(original, updated).generateDiff() var index = 0 var indexInOriginalSequence = 0 val operations = mutableListOf>() - - diff.forEach { operation -> + diff.fastForEach { operation -> when (operation) { is Insert -> { operations += Add( @@ -48,13 +47,9 @@ internal object DiffGenerator { } } - if (detectMoves) { - reduceDeletesAndAddsToMoves(operations) - } - - return DiffResult( - operations = reduceSequences(operations) - ) + if (detectMoves) reduceDeletesAndAddsToMoves(operations) + reduceSequences(operations) + return DiffResult(operations) } /** @@ -82,10 +77,6 @@ internal object DiffGenerator { while (index < operations.size) { val operation = operations[index] - check(operation is Add || operation is Remove) { - "Only add and remove operations should appear in the diff" - } - var indexOfOppositeAction = index + 1 var endIndexDifference = 0 @@ -137,10 +128,7 @@ internal object DiffGenerator { else -> false } - private fun reduceSequences( - operations: MutableList> - ): List> { - val result = mutableListOf>() + private fun reduceSequences(operations: MutableList>) { var index = 0 while (index < operations.size) { @@ -154,62 +142,49 @@ internal object DiffGenerator { sequenceLength++ } - result += reduceSequence( - operations = operations, - sequenceStartIndex = index, - sequenceEndIndex = sequenceEndIndex - ) + if (sequenceLength > 1) { + operations[index] = reduceSequence( + operations = operations, + sequenceStartIndex = index, + sequenceLength = sequenceLength + ) - index += sequenceLength - } + repeat(sequenceLength - 1) { operations.removeAt(index + 1) } + } - return result + index++ + } } private fun reduceSequence( operations: MutableList>, sequenceStartIndex: Int, - sequenceEndIndex: Int - ): DiffOperation { - val sequenceLength = sequenceEndIndex - sequenceStartIndex - return if (sequenceLength == 1) { - operations[sequenceStartIndex] - } else { - when (val startOperation = operations[sequenceStartIndex]) { - is Remove -> { - RemoveRange( - startIndex = startOperation.index, - endIndex = startOperation.index + sequenceLength - ) - } - is Add -> { - AddAll( - index = startOperation.index, - items = operations.subList(sequenceStartIndex, sequenceEndIndex) - .asSequence() - .map { operation -> - require(operation is Add) { - "Cannot reduce $operation as part of an insert sequence " + - "because it is not an add action." - } - - operation.item - } - .toList() - ) - } - is Move -> { - MoveRange( - fromIndex = startOperation.fromIndex, - toIndex = startOperation.toIndex, - itemCount = sequenceLength - ) + sequenceLength: Int + ): DiffOperation = when (val startOperation = operations[sequenceStartIndex]) { + is Remove -> { + RemoveRange( + startIndex = startOperation.index, + endIndex = startOperation.index + sequenceLength + ) + } + is Add -> { + AddAll( + index = startOperation.index, + items = List(sequenceLength) { i -> + (operations[sequenceStartIndex + i] as Add).item } - else -> throw IllegalArgumentException( - "Cannot reduce sequence starting with $startOperation" - ) - } + ) } + is Move -> { + MoveRange( + fromIndex = startOperation.fromIndex, + toIndex = startOperation.toIndex, + itemCount = sequenceLength + ) + } + else -> throw IllegalArgumentException( + "Cannot reduce sequence starting with $startOperation" + ) } private fun DiffOperation.canBeCombinedWith( diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/CircularIntArray.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/CircularIntArray.kt index 67d215d..b9c5ba6 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/CircularIntArray.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/CircularIntArray.kt @@ -3,17 +3,17 @@ package dev.andrewbailey.diff.impl import kotlin.jvm.JvmInline @JvmInline -internal value class CircularIntArray(private val array: IntArray) { +internal value class CircularIntArray(val array: IntArray) { constructor(size: Int) : this(IntArray(size)) - operator fun get(index: Int): Int = array[toInternalIndex(index)] + inline operator fun get(index: Int): Int = array[toLinearIndex(index)] - operator fun set(index: Int, value: Int) { - array[toInternalIndex(index)] = value + inline operator fun set(index: Int, value: Int) { + array[toLinearIndex(index)] = value } - private fun toInternalIndex(index: Int): Int { + private inline fun toLinearIndex(index: Int): Int { val moddedIndex = index % array.size return if (moddedIndex < 0) { moddedIndex + array.size diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Extensions.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Extensions.kt index ce52cdf..ea3cd31 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Extensions.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Extensions.kt @@ -2,17 +2,26 @@ package dev.andrewbailey.diff.impl import kotlin.math.abs -internal fun Int.isEven() = abs(this) % 2 == 0 +internal inline fun Int.isEven() = abs(this) % 2 == 0 -internal fun Int.isOdd() = abs(this) % 2 == 1 +internal inline fun Int.isOdd() = abs(this) % 2 == 1 -internal fun MutableList.push(item: T) { +internal inline fun MutableList.push(item: T) { add(item) } -internal fun MutableList.pop(): T { - check(isNotEmpty()) { - "List has no items" +internal inline fun MutableList.pop(): T = removeAt(size - 1) + +internal inline fun List.fastForEach(action: (T) -> Unit) { + @Suppress("ReplaceManualRangeWithIndicesCalls") + for (i in 0 until size) { + action(this[i]) + } +} + +internal inline fun List.fastForEachIndexed(action: (Int, T) -> Unit) { + @Suppress("ReplaceManualRangeWithIndicesCalls") + for (i in 0 until size) { + action(i, this[i]) } - return removeAt(size - 1) } diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithm.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithm.kt index 1f55a48..dee7670 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithm.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithm.kt @@ -38,21 +38,11 @@ internal class MyersDiffAlgorithm( private val updated: List ) { - fun generateDiff(): Sequence> = walkSnakes() - .asSequence() - .map { (x1, y1, x2, y2) -> - when { - x1 == x2 -> Insert(value = updated[y1]) - y1 == y2 -> Delete - else -> Skip - } - } - - private fun walkSnakes(): List { + fun generateDiff(): List> { val path = findPath() + val regions = mutableListOf>() - val regions = mutableListOf() - path.forEach { (p1, p2) -> + path.fastForEach { (p1, p2) -> var (x1, y1) = walkDiagonal(p1, p2, regions) val (x2, y2) = p2 @@ -60,11 +50,11 @@ internal class MyersDiffAlgorithm( val dX = x2 - x1 when { dY > dX -> { - regions += Region(x1, y1, x1, y1 + 1) + regions += interpretRegion(x1, y1, x1, y1 + 1) y1++ } dY < dX -> { - regions += Region(x1, y1, x1 + 1, y1) + regions += interpretRegion(x1, y1, x1 + 1, y1) x1++ } } @@ -78,12 +68,12 @@ internal class MyersDiffAlgorithm( private fun walkDiagonal( start: Point, end: Point, - regionsOutput: MutableList + regionsOutput: MutableList> ): Point { var (x1, y1) = start val (x2, y2) = end while (x1 < x2 && y1 < y2 && original[x1] == updated[y1]) { - regionsOutput += Region(x1, y1, x1 + 1, y1 + 1) + regionsOutput += interpretRegion(x1, y1, x1 + 1, y1 + 1) x1++ y1++ } @@ -91,6 +81,17 @@ internal class MyersDiffAlgorithm( return Point(x1, y1) } + private fun interpretRegion( + x1: Int, + y1: Int, + x2: Int, + y2: Int + ): MyersDiffOperation = when { + x1 == x2 -> Insert(value = updated[y1]) + y1 == y2 -> Delete + else -> Skip + } + private fun findPath(): List { val snakes = mutableListOf() val stack = mutableListOf() @@ -128,14 +129,7 @@ internal class MyersDiffAlgorithm( } } - snakes.sortWith(object : Comparator { - override fun compare(a: Snake, b: Snake): Int = if (a.start.x == b.start.x) { - a.start.y - b.start.y - } else { - a.start.x - b.start.x - } - }) - + snakes.sort() return snakes } diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Point.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Point.kt index a76afd4..03743e1 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Point.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Point.kt @@ -1,6 +1,16 @@ package dev.andrewbailey.diff.impl -internal data class Point( - val x: Int, - val y: Int -) +import kotlin.jvm.JvmInline + +@JvmInline +internal value class Point(private val packed: Long) { + val x: Int get() = (packed and 0xFFFFFFFF).toInt() + val y: Int get() = (packed shr 32).toInt() + + constructor(x: Int, y: Int) : this( + (x.toLong() and 0xFFFFFFFF) or (y.toLong() shl 32) + ) + + inline operator fun component1() = x + inline operator fun component2() = y +} diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Snake.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Snake.kt index da333a9..d189f23 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Snake.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/impl/Snake.kt @@ -3,4 +3,10 @@ package dev.andrewbailey.diff.impl internal data class Snake( val start: Point, val end: Point -) +) : Comparable { + override fun compareTo(other: Snake): Int = if (start.x == other.start.x) { + start.y - other.start.y + } else { + start.x - other.start.x + } +} diff --git a/difference/src/commonTest/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithmTest.kt b/difference/src/commonTest/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithmTest.kt index 0555bc3..a81521e 100644 --- a/difference/src/commonTest/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithmTest.kt +++ b/difference/src/commonTest/kotlin/dev/andrewbailey/diff/impl/MyersDiffAlgorithmTest.kt @@ -131,7 +131,7 @@ class MyersDiffAlgorithmTest { ) } - private fun applyDiff(original: List, diff: Sequence>): List = + private fun applyDiff(original: List, diff: List>): List = original.toMutableList().apply { var index = 0 diff.forEach { operation -> diff --git a/difference/src/jvmMain/kotlin/dev/andrewbailey/diff/DiffReceiver.kt b/difference/src/jvmMain/kotlin/dev/andrewbailey/diff/DiffReceiver.kt index 3829537..9fbedae 100644 --- a/difference/src/jvmMain/kotlin/dev/andrewbailey/diff/DiffReceiver.kt +++ b/difference/src/jvmMain/kotlin/dev/andrewbailey/diff/DiffReceiver.kt @@ -6,6 +6,8 @@ import dev.andrewbailey.diff.DiffOperation.Move import dev.andrewbailey.diff.DiffOperation.MoveRange import dev.andrewbailey.diff.DiffOperation.Remove import dev.andrewbailey.diff.DiffOperation.RemoveRange +import dev.andrewbailey.diff.impl.fastForEach +import dev.andrewbailey.diff.impl.fastForEachIndexed /** * This class serves as a convenience class for Java users who may find it tedious to call @@ -18,7 +20,7 @@ import dev.andrewbailey.diff.DiffOperation.RemoveRange abstract class DiffReceiver { fun applyDiff(diff: DiffResult) { - diff.operations.forEach { operation -> + diff.operations.fastForEach { operation -> when (operation) { is Remove -> { remove(operation.index) @@ -53,7 +55,7 @@ abstract class DiffReceiver { abstract fun insert(item: T, index: Int) open fun insertAll(items: List, index: Int) { - items.forEachIndexed { itemIndex, item -> + items.fastForEachIndexed { itemIndex, item -> insert(item, index + itemIndex) } }