commonMain.dev.inmo.micro_utils.common.DiffUtils.kt Maven / Gradle / Ivy
@file:Suppress("NOTHING_TO_INLINE")
package dev.inmo.micro_utils.common
import kotlinx.serialization.Serializable
private inline fun getObject(
additional: MutableList,
iterator: Iterator
): T? = when {
additional.isNotEmpty() -> additional.removeAt(0)
iterator.hasNext() -> iterator.next()
else -> null
}
/**
* Diff object which contains information about differences between two [Iterable]s
*
* 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
*
* @see calculateDiff
*/
@Serializable
data class Diff @Warning(warning) constructor(
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue>,
/**
* Old-New values pairs
*/
val replaced: List, @Serializable(IndexedValueSerializer::class) IndexedValue>>,
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue>
) {
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
companion object {
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
}
}
fun emptyDiff(): Diff = Diff(emptyList(), emptyList(), emptyList())
private inline fun performChanges(
potentialChanges: MutableList?, IndexedValue?>>,
additionsInOld: MutableList,
additionsInNew: MutableList,
changedList: MutableList, IndexedValue>>,
removedList: MutableList>,
addedList: MutableList>,
comparisonFun: (T?, T?) -> Boolean
) {
var i = -1
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
i++
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
changedList.addAll(
potentialChanges.take(i).mapNotNull {
@Suppress("UNCHECKED_CAST")
if (it.first != null && it.second != null) it as Pair, IndexedValue> 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!!)
oldOne ?.let { additionsInOld.add(oldOne.value) }
}
if (newPotentials.size > 1) {
newPotentials.last().first ?.value ?.let { additionsInOld.add(it) }
}
}
newOneEqualToOldObject -> {
newPotentials.first().first ?.let { removedList.add(it) }
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
removedList.add(oldOne!!)
newOne ?.let { additionsInNew.add(newOne.value) }
}
if (newPotentials.size > 1) {
newPotentials.last().second ?.value ?.let { additionsInNew.add(it) }
}
}
}
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 Iterable.calculateDiff(
other: Iterable,
comparisonFun: (T?, T?) -> Boolean
): Diff {
var i = -1
var j = -1
val additionalInOld = mutableListOf()
val additionalInNew = mutableListOf()
val oldIterator = iterator()
val newIterator = other.iterator()
val potentiallyChangedObjects = mutableListOf?, IndexedValue?>>()
val changedObjects = mutableListOf, IndexedValue>>()
val addedObjects = mutableListOf>()
val removedObjects = mutableListOf>()
while (true) {
i++
j++
val oldObject = getObject(additionalInOld, oldIterator)
val newObject = getObject(additionalInNew, newIterator)
if (oldObject == null && newObject == null) {
break
}
when {
comparisonFun(oldObject, newObject) -> {
changedObjects.addAll(potentiallyChangedObjects.map {
@Suppress("UNCHECKED_CAST")
it as Pair, IndexedValue>
})
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, comparisonFun)
i -= (additionalInOld.size - previousOldsAdditionsSize)
j -= (additionalInNew.size - previousNewsAdditionsSize)
}
}
}
potentiallyChangedObjects.add(null to null)
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
}
/**
* 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 Iterable.calculateDiff(
other: Iterable,
strictComparison: Boolean = false
): Diff = calculateDiff(
other,
comparisonFun = if (strictComparison) {
{ t1, t2 ->
t1 === t2
}
} else {
{ t1, t2 ->
t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially
}
}
)
inline fun Iterable.diff(
other: Iterable,
strictComparison: Boolean = false
): Diff = calculateDiff(other, strictComparison)
inline fun Iterable.diff(
other: Iterable,
noinline comparisonFun: (T?, T?) -> Boolean
): Diff = calculateDiff(other, comparisonFun)
inline fun Diff(old: Iterable, new: Iterable) = old.calculateDiff(new, strictComparison = false)
inline fun StrictDiff(old: Iterable, new: Iterable) = old.calculateDiff(new, true)
/**
* This method call [calculateDiff] with strict mode enabled
*/
inline fun Iterable.calculateStrictDiff(
other: Iterable
) = calculateDiff(other, strictComparison = true)
/**
* Applies [diff] to [this] [MutableList]
*/
fun MutableList.applyDiff(
diff: Diff
) {
for (i in diff.removed.indices.sortedDescending()) {
removeAt(diff.removed[i].index)
}
diff.added.forEach { (i, t) ->
add(i, t)
}
diff.replaced.forEach { (_, new) ->
set(new.index, new.value)
}
}
/**
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
* mutable list
*/
fun MutableList.applyDiff(
source: Iterable,
strictComparison: Boolean = false
): Diff = calculateDiff(source, strictComparison).also {
applyDiff(it)
}
/**
* This method call [calculateDiff] and then apply differences to [this]
* mutable list
*/
fun MutableList.applyDiff(
source: Iterable,
comparisonFun: (T?, T?) -> Boolean
): Diff = calculateDiff(source, comparisonFun).also {
applyDiff(it)
}
/**
* Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the
* [Diff.replaced] values will be reversed too
*/
fun Diff.reversed() = Diff(
removed = added,
replaced = replaced.map { it.second to it.first },
added = removed
)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy