diff --git a/CHANGELOG.md b/CHANGELOG.md index 94ffe51e19d..85a096e0561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ * `Versions` * `Serialization`: `1.0.0` -> `1.0.1` +* `Common` + * Full rework of `DiffUtils` + * Data class `Diff` has been added + * Extension `Iterable#calculateDiff` has been added + * Extension `Iterable#calculateStrictDiff` as replacement for `Iterable#calculateDiff` with + `strictComparison` mode enabled + * Functions `Diff` (as analog of `Iterable#calculateDiff`) and `StrictDiff` (as analog of + `Iterable#calculateStrictDiff`) * `Coroutines` * `BroadcastFlow` now is deprecated * `BroadcastStateFlow` now is deprecated diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt index 92e0dca214d..39477a272d2 100644 --- a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/DiffUtils.kt @@ -1,10 +1,166 @@ +@file:Suppress("NOTHING_TO_INLINE") + package dev.inmo.micro_utils.common -fun Iterable.syncWith( - other: Iterable, - removed: (List) -> Unit = {}, - added: (List) -> Unit = {} -) { - removed(filter { it !in other }) - added(other.filter { it !in this }) +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 calculateDiff + */ +data class Diff internal constructor( + val removed: List>, + /** + * Old-New values pairs + */ + val replaced: List, IndexedValue>>, + val added: List> +) + +private inline fun performChanges( + potentialChanges: MutableList?, IndexedValue?>>, + additionalsInOld: MutableList, + additionalsInNew: MutableList, + changedList: MutableList, IndexedValue>>, + removedList: MutableList>, + addedList: MutableList>, + 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 { + 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 { additionalsInOld.add(oldOne.value) } + } + if (newPotentials.size > 1) { + newPotentials.last().first ?.value ?.let { additionalsInOld.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 { additionalsInNew.add(newOne.value) } + } + if (newPotentials.size > 1) { + newPotentials.last().second ?.value ?.let { additionalsInNew.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, + strictComparison: Boolean = false +): 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 { + oldObject === newObject || (oldObject == newObject && !strictComparison) -> { + changedObjects.addAll(potentiallyChangedObjects.map { 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, 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()) +} + +inline fun Diff(old: Iterable, new: Iterable) = old.calculateDiff(new) +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, true) + +/** + * Compare one-to-one + */ +@Deprecated("Will be removed or replaced with some new function. Use calculateDiff instead") +inline fun Iterable.syncWith( + other: Iterable, + noinline removed: (List) -> Unit = {}, + noinline added: (List) -> Unit = {} +) { + calculateDiff(other).also { + removed(it.removed.map { it.value }) + added(it.added.map { it.value }) + } } diff --git a/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt b/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt new file mode 100644 index 00000000000..f4568073341 --- /dev/null +++ b/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt @@ -0,0 +1,80 @@ +package dev.inmo.micro_utils.common + +import kotlin.math.floor +import kotlin.test.Test +import kotlin.test.assertEquals + +class DiffUtilsTests { + @Test + fun testThatSimpleRemoveWorks() { + val oldList = (0 until 10).toList() + val withIndex = oldList.withIndex() + + for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { + for ((i, v) in withIndex) { + if (i + count > oldList.lastIndex) { + continue + } + val removedSublist = oldList.subList(i, i + count) + oldList.calculateNonstrictDiff(oldList - removedSublist).apply { + assertEquals( + removedSublist.mapIndexed { j, o -> IndexedValue(i + j, o) }, + removed + ) + } + } + } + } + + /** + * In this test was used [calculateDiff] parameter `strictComparison`. That is required to be sure that the same + * objects with different links will be used as different objects in `strictComparison` mode + */ + @Test + fun testThatSimpleAddWorks() { + val oldList = (0 until 10).map { it.toString() } + val withIndex = oldList.withIndex() + + for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { + for ((i, v) in withIndex) { + if (i + count > oldList.lastIndex) { + continue + } + val addedSublist = oldList.subList(i, i + count) + val mutable = oldList.toMutableList() + mutable.addAll(i, oldList.subList(i, i + count).map { it.toCharArray().concatToString() }) + oldList.calculateStrictDiff(mutable).apply { + assertEquals( + addedSublist.mapIndexed { j, o -> IndexedValue(i + j, o) }, + added + ) + } + } + } + } + + @Test + fun testThatSimpleChangesWorks() { + val oldList = (0 until 10).map { it.toString() } + val withIndex = oldList.withIndex() + + for (step in 0 until oldList.size) { + for ((i, v) in withIndex) { + val mutable = oldList.toMutableList() + val changes = ( + if (step == 0) i until oldList.size else (i until oldList.size step step) + ).map { index -> + IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { + mutable[index] = it.value + } + } + oldList.calculateNonstrictDiff(mutable).apply { + assertEquals( + changes, + replaced + ) + } + } + } + } +}