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/DiffOperation.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffOperation.kt index 2a5dcd2..9fc8f59 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffOperation.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffOperation.kt @@ -1,32 +1,136 @@ package dev.andrewbailey.diff +/** + * An individual operation contained in a [DiffResult] between two collections. + */ sealed class DiffOperation { + /** + * 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( val index: Int, val item: T ) : DiffOperation() + /** + * 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( val startIndex: Int, val endIndex: Int ) : DiffOperation() + /** + * 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( val index: Int, val item: T ) : DiffOperation() + /** + * 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( val index: Int, val items: List ) : DiffOperation() + /** + * 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( val fromIndex: Int, val toIndex: Int ) : DiffOperation() + /** + * 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( val fromIndex: Int, val toIndex: Int, diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffResult.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffResult.kt index ba1316b..ae64111 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffResult.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/DiffResult.kt @@ -1,7 +1,42 @@ package dev.andrewbailey.diff -class DiffResult internal constructor(val operations: List>) { +/** + * Stores the result of a diff calculated by [differenceOf]. + */ +class DiffResult 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> +) { + /** + * 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, @@ -38,6 +73,40 @@ class DiffResult internal constructor(val operations: List>) ) } + /** + * 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, diff --git a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/Difference.kt b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/Difference.kt index 488e141..ef462c6 100644 --- a/difference/src/commonMain/kotlin/dev/andrewbailey/diff/Difference.kt +++ b/difference/src/commonMain/kotlin/dev/andrewbailey/diff/Difference.kt @@ -4,6 +4,33 @@ package dev.andrewbailey.diff import kotlin.jvm.JvmName +/** + * Constructs a diff between the [original] and [updated] list inputs. The returned [DiffResult] + * represents a sequence of operations which, if applied in order on the [original] list will + * yield the [updated] list. The returned list always contains the minimum number of operations + * required to transform the original input to the updated input. + * + * Optionally, move operations can be enabled or disabled by specifying [detectMoves]. If disabled, + * the diff result will only include add and delete operations, which may cause the same item to + * be deleted and re-inserted. If movement detection is enabled, all objects shared between the + * two lists will either remain in place or be moved within the list instead of removing and + * reinserting them. + * + * Internally, this function performs the Eugene-Myers diffing algorithm. the worst case runtime + * of the algorithm takes on the order of O((M+N)×D + D log D) operations. M and N are the lengths + * of the two input lists, and D is the smallest number of operations that it takes to modify the + * original list into the updated one. If move detection is enabled, add another O(D²) to that + * runtime. + * + * @param original The "before" state of the list to be diffed. For optimal performance, it is + * critical that this data structure supports efficient random reads. + * @param updated The "after" state of the list to be diffed. For optimal performance, it is + * critical that this data structure supports efficient random reads. + * @param detectMoves Whether or not to detect moved objects as such. When disabled, the returned + * diff will only contain insert and delete operations. Enabled by default. + * @return A [DiffResult] containing a shortest possible sequence of operations that can transform + * the "before" state of the list into the "after" state of the list. + */ fun differenceOf( original: List, updated: List, 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) } } diff --git a/gradle.properties b/gradle.properties index 89e6998..c1c29bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ kotlin.code.style=official android.enableAdditionalTestOutput=true GROUP=dev.andrewbailey.difference -VERSION_NAME=1.0.0 +VERSION_NAME=1.1.0 POM_ARTIFACT_ID=difference POM_NAME=Difference