@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.removeFirst() 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 internal 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() } 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 )