Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -18,14 +19,12 @@ internal object DiffGenerator {
updated: List<T>,
detectMoves: Boolean
): DiffResult<T> {
val diff = MyersDiffAlgorithm(original, updated)
.generateDiff()
val diff = MyersDiffAlgorithm(original, updated).generateDiff()

var index = 0
var indexInOriginalSequence = 0
val operations = mutableListOf<DiffOperation<T>>()

diff.forEach { operation ->
diff.fastForEach { operation ->
when (operation) {
is Insert -> {
operations += Add(
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -82,10 +77,6 @@ internal object DiffGenerator {
while (index < operations.size) {
val operation = operations[index]

check(operation is Add<T> || operation is Remove<T>) {
"Only add and remove operations should appear in the diff"
}

var indexOfOppositeAction = index + 1
var endIndexDifference = 0

Expand Down Expand Up @@ -137,10 +128,7 @@ internal object DiffGenerator {
else -> false
}

private fun <T> reduceSequences(
operations: MutableList<DiffOperation<T>>
): List<DiffOperation<T>> {
val result = mutableListOf<DiffOperation<T>>()
private fun <T> reduceSequences(operations: MutableList<DiffOperation<T>>) {
var index = 0

while (index < operations.size) {
Expand All @@ -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 <T> reduceSequence(
operations: MutableList<DiffOperation<T>>,
sequenceStartIndex: Int,
sequenceEndIndex: Int
): DiffOperation<T> {
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<T>) {
"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<T> = 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<T>).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 <T> DiffOperation<T>.canBeCombinedWith(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,136 @@
package dev.andrewbailey.diff

/**
* An individual operation contained in a [DiffResult] between two collections.
*/
sealed class DiffOperation<T> {

/**
* An operation in a diff to indicate the removal of an object between the before and after
* states of the collections.
*
* @param index The index of the removal, expressed as the index to remove from the collection
* if you had applied all previous operations in this diff.
* @param item The item that was removed from the original collection
*/
data class Remove<T>(
val index: Int,
val item: T
) : DiffOperation<T>()

/**
* An operation in a diff to indicate the removal of a range of objects between the before and
* after states of the collections.
*
* @param startIndex The start index of the removal (inclusive), expressed as the index to
* remove from the collection if you had applied all previous operations in this diff.
* @param endIndex The end index of the removal (exclusive), expressed as the index to remove
* from the collection if you had applied all previous operations in this diff.
*/
data class RemoveRange<T>(
val startIndex: Int,
val endIndex: Int
) : DiffOperation<T>()

/**
* An operation in a diff to indicate the insertion of an object between the before and after
* states of the collections.
*
* @param index The index of the insertion, expressed as the index to add the given object to
* the collection if you had applied all previous operations in this diff.
* @param item The object to insert at the specified location.
*/
data class Add<T>(
val index: Int,
val item: T
) : DiffOperation<T>()

/**
* An operation in a diff to indicate the insertion of several objects between the before and
* after states of the collections.
*
* @param index The index of the insertions, expressed as the index to add the given object to
* the collection if you had applied all previous operations in this diff. The first object in
* the given items list should be inserted at this index, with all subsequent operations
* following consecutively.
* @param items The objects to insert at the specified location. Always contains at least two
* values.
*/
data class AddAll<T>(
val index: Int,
val items: List<T>
) : DiffOperation<T>()

/**
* An operation in a diff to indicate the movement of one object to a new index between the
* before and after states of the collections. Only included in a diff if movement detection
* was enabled.
*
* The indices expressed in this object assume applying the move operation as an atomic
* operation to a collection that has had all other operations in this diff applied. To
* implement as a remove-then-add call, correct the [toIndex] as shown:
*
* ```kotlin
* add(removeAt(fromIndex), if (toIndex < fromIndex) toIndex else toIndex - 1)
* ```
*
* @param fromIndex The index of the object as it would appear if you had applied all previous
* operations in this diff.
* @param toIndex The new index that this object should appear at, not accounting for the
* deletion of this object from its current location.
*/
data class Move<T>(
val fromIndex: Int,
val toIndex: Int
) : DiffOperation<T>()

/**
* An operation in a diff to indicate the movement of a range of objects to a new location
* between the before and after states of the collections. Only included in a diff if movement
* detection was enabled.
*
* The indices expressed in this object assume applying the move operation as an atomic
* operation to a collection that has had all other operations in this diff applied. To
* implement as a removeRange-then-add call, correct the [toIndex] as shown:
*
* ```kotlin
* addAll(
* values = removeRange(fromIndex, itemCount),
* index = if (toIndex < fromIndex) toIndex else toIndex - 1
* )
* ```
*
* Alternatively, to implement as a loop of remove-then-add calls, correct the destination
* indices as shown:
*
* ```kotlin
* when {
* toIndex < fromIndex -> {
* (0 until count).forEach { item ->
* val oldIndex = fromIndex + item
* val newIndex = toIndex + item
* add(
* item = removeAt(oldIndex),
* index = if (newIndex < oldIndex) newIndex else newIndex - 1
* )
* }
* }
* toIndex > fromIndex -> {
* repeat(count) {
* add(
* item = removeAt(fromIndex),
* index = newIndex
* )
* }
* }
* }
* ```
*
* @param fromIndex The index of the object as it would appear if you had applied all previous
* operations in this diff.
* @param toIndex The new index that this object should appear at, not accounting for the
* deletion of this object from its current location.
*/
data class MoveRange<T>(
val fromIndex: Int,
val toIndex: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
package dev.andrewbailey.diff

class DiffResult<T> internal constructor(val operations: List<DiffOperation<T>>) {
/**
* Stores the result of a diff calculated by [differenceOf].
*/
class DiffResult<T> internal constructor(
/**
* The sequence of operations in the diff. When applied in order, these operations will
* transform the original collection into the target collection. This list always coalesces
* ranges of the same operation together, and prefers to use the minimum number of
* [DiffOperation] objects required to express the diff.
*/
val operations: List<DiffOperation<T>>
) {

/**
* Executes the operations in this diff-in order. This function is generally intended to
* mirror the state changes expressed in this difference to a parallel data structure derived
* by the diff-ed collection.
*
* The lambdas passed into this method are invoked to respond to each operation. When an index
* is specified, it expresses the index of the in-flight data structure. That is, an index
* which is only valid when all previously dispatched operations have been applied to the
* original collection.
*
* This overload applies operations on an element-by-element basis. Use the overload with
* the additional "Range" lambdas to support bulk operations, which can improve performance.
*
* @param remove Called to remove the object at the specified `index` from the in-flight
* collection as a step towards the final collection.
* @param insert Called to insert the given `item` at the specified `index` to the in-flight
* collection as a step towards the final collection.
* @param move If this diff was generated with moves detected, this function is called to move
* the item at `oldIndex` to `newIndex`. If this diff was not generated with moves detected,
* this lambda will never be invoked. These indexes are written assuming an atomic operation
* to perform the move. To implement a move operation as a remove and re-insert operation, the
* `newIndex` may need to be corrected with an offset. In practice, this looks like
* `add(removeAt(oldIndex), if (newIndex < oldIndex) else newIndex - 1)`.
*/
inline fun applyDiff(
crossinline remove: (index: Int) -> Unit,
crossinline insert: (item: T, index: Int) -> Unit,
Expand Down Expand Up @@ -38,6 +73,40 @@ class DiffResult<T> internal constructor(val operations: List<DiffOperation<T>>)
)
}

/**
* Executes the operations in this diff-in order, with support for applying operations in bulk
* when a large span of items experience the same transformation. This function is generally
* intended to mirror the state changes expressed in this difference to a parallel data
* structure derived by the diff-ed collection.
*
* The lambdas passed into this method are invoked to respond to each aggregated operation.
* When an index or range is specified, it is indexed to the in-flight data structure. That is,
* an index which is only valid when all previously dispatched operations have been applied
* to the original collection.
*
* @param remove Called to remove the object at the specified `index` from the in-flight
* collection as a step towards the final collection.
* @param removeRange Called to remove all objects between index `start` (inclusive) and `end`
* (exclusive) from the in-flight collection as a step towards the final collection.
* @param insert Called to insert the given `item` at the specified `index` to the in-flight
* collection as a step towards the final collection.
* @param insertAll Called to insert all items in the provided collection to the in-flight
* collection. The given list of items should be added in-order with the inserted sequence
* starting at the provided `index` in the in-flight collection.
* @param move If this diff was generated with moves detected, this function is called to move
* the item at `oldIndex` to `newIndex`. If this diff was not generated with moves detected,
* this lambda will never be invoked. These indexes are written assuming an atomic operation
* to perform the move. To implement a move operation as a remove and re-insert operation, the
* `newIndex` may need to be corrected with an offset. In practice, this looks like
* `add(removeAt(oldIndex), if (newIndex < oldIndex) newIndex else newIndex - 1)`.
* @param moveRange This lambda is used in the same way as [move] is, but operates on a
* contiguous span of two or more items to be moved. The indexing works in the same way as it
* does for the single-item variant, in that the `newIndex` and `oldIndex` arguments provided
* are both specified with respect to the current state of the in-flight array. If implementing
* this expression as an addRange and removeRange operation, or by breaking this operation into
* multiple move operations, you will need to correct the destination index when it is before
* the source index.
*/
inline fun applyDiff(
crossinline remove: (index: Int) -> Unit,
crossinline removeRange: (start: Int, end: Int) -> Unit,
Expand Down
Loading
Loading