MicroUtils/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt

190 lines
7.7 KiB
Kotlin
Raw Normal View History

2020-11-01 18:12:37 +00:00
@file:Suppress("NOTHING_TO_INLINE")
2020-09-26 16:19:20 +00:00
package dev.inmo.micro_utils.common
2023-01-20 07:29:45 +00:00
import kotlinx.serialization.Serializable
2020-11-01 18:12:37 +00:00
private inline fun <T> getObject(
additional: MutableList<T>,
iterator: Iterator<T>
): T? = when {
additional.isNotEmpty() -> additional.removeFirst()
iterator.hasNext() -> iterator.next()
else -> null
}
/**
* Diff object which contains information about differences between two [Iterable]s
*
2022-09-23 06:10:24 +00:00
* See tests for more info
*
* @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection
* @param added The object which appear in new collection only. Indexes here show the index in the new collection
* @param replaced Pair of old-new changes. First object has been presented in the old collection on its
* [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in
* case when some value has been replaced after adds or removes in original collection the object index will be changed
*
2020-11-01 18:12:37 +00:00
* @see calculateDiff
*/
2023-01-20 07:29:45 +00:00
@Serializable
2020-11-01 18:12:37 +00:00
data class Diff<T> internal constructor(
2023-01-20 07:29:45 +00:00
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
2020-11-01 18:12:37 +00:00
/**
* Old-New values pairs
*/
2023-01-20 07:29:45 +00:00
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
2020-11-01 18:12:37 +00:00
)
private inline fun <T> performChanges(
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
2021-09-08 06:10:32 +00:00
additionsInOld: MutableList<T>,
additionsInNew: MutableList<T>,
2020-11-01 18:12:37 +00:00
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
removedList: MutableList<IndexedValue<T>>,
addedList: MutableList<IndexedValue<T>>,
strictComparison: Boolean
) {
var i = -1
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
i++
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
changedList.addAll(
potentialChanges.take(i).mapNotNull {
2022-04-27 09:15:03 +00:00
@Suppress("UNCHECKED_CAST")
2020-11-01 18:12:37 +00:00
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
}
)
val newPotentials = potentialChanges.drop(i).take(potentialChanges.size - i)
when {
oldOneEqualToNewObject -> {
newPotentials.first().second ?.let { addedList.add(it) }
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
addedList.add(newOne!!)
2021-09-08 06:10:32 +00:00
oldOne ?.let { additionsInOld.add(oldOne.value) }
2020-11-01 18:12:37 +00:00
}
if (newPotentials.size > 1) {
2021-09-08 06:10:32 +00:00
newPotentials.last().first ?.value ?.let { additionsInOld.add(it) }
2020-11-01 18:12:37 +00:00
}
}
newOneEqualToOldObject -> {
newPotentials.first().first ?.let { removedList.add(it) }
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
removedList.add(oldOne!!)
2021-09-08 06:10:32 +00:00
newOne ?.let { additionsInNew.add(newOne.value) }
2020-11-01 18:12:37 +00:00
}
if (newPotentials.size > 1) {
2021-09-08 06:10:32 +00:00
newPotentials.last().second ?.value ?.let { additionsInNew.add(it) }
2020-11-01 18:12:37 +00:00
}
}
}
potentialChanges.clear()
return
}
}
if (potentialChanges.isNotEmpty() && potentialChanges.last().let { it.first == null && it.second == null }) {
potentialChanges.dropLast(1).forEach { (old, new) ->
when {
old != null && new != null -> changedList.add(old to new)
old != null -> removedList.add(old)
new != null -> addedList.add(new)
}
}
}
}
/**
* Calculating [Diff] object
*
* @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different
* objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links
*/
fun <T> Iterable<T>.calculateDiff(
other: Iterable<T>,
strictComparison: Boolean = false
): Diff<T> {
var i = -1
var j = -1
val additionalInOld = mutableListOf<T>()
val additionalInNew = mutableListOf<T>()
val oldIterator = iterator()
val newIterator = other.iterator()
val potentiallyChangedObjects = mutableListOf<Pair<IndexedValue<T>?, IndexedValue<T>?>>()
val changedObjects = mutableListOf<Pair<IndexedValue<T>, IndexedValue<T>>>()
val addedObjects = mutableListOf<IndexedValue<T>>()
val removedObjects = mutableListOf<IndexedValue<T>>()
while (true) {
i++
j++
val oldObject = getObject(additionalInOld, oldIterator)
val newObject = getObject(additionalInNew, newIterator)
if (oldObject == null && newObject == null) {
break
}
when {
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
2022-04-27 09:15:03 +00:00
changedObjects.addAll(potentiallyChangedObjects.map {
@Suppress("UNCHECKED_CAST")
it as Pair<IndexedValue<T>, IndexedValue<T>>
})
2020-11-01 18:12:37 +00:00
potentiallyChangedObjects.clear()
}
else -> {
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
val previousOldsAdditionsSize = additionalInOld.size
val previousNewsAdditionsSize = additionalInNew.size
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
i -= (additionalInOld.size - previousOldsAdditionsSize)
j -= (additionalInNew.size - previousNewsAdditionsSize)
}
}
}
potentiallyChangedObjects.add(null to null)
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
}
2021-09-08 06:10:32 +00:00
inline fun <T> Iterable<T>.diff(
other: Iterable<T>,
strictComparison: Boolean = false
): Diff<T> = calculateDiff(other, strictComparison)
2020-11-01 18:12:37 +00:00
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
/**
* This method call [calculateDiff] with strict mode enabled
*/
inline fun <T> Iterable<T>.calculateStrictDiff(
other: Iterable<T>
2020-11-01 18:19:23 +00:00
) = calculateDiff(other, strictComparison = true)
/**
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
* mutable list
*/
fun <T> MutableList<T>.applyDiff(
source: Iterable<T>,
strictComparison: Boolean = false
2022-09-23 10:05:32 +00:00
): Diff<T> = calculateDiff(source, strictComparison).also {
for (i in it.removed.indices.sortedDescending()) {
removeAt(it.removed[i].index)
}
it.added.forEach { (i, t) ->
add(i, t)
}
it.replaced.forEach { (_, new) ->
set(new.index, new.value)
}
}