mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-27 10:11:22 +00:00 
			
		
		
		
	diff utils rewriting
This commit is contained in:
		| @@ -4,6 +4,14 @@ | |||||||
|  |  | ||||||
| * `Versions` | * `Versions` | ||||||
|     * `Serialization`: `1.0.0` -> `1.0.1` |     * `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` | * `Coroutines` | ||||||
|     * `BroadcastFlow` now is deprecated |     * `BroadcastFlow` now is deprecated | ||||||
|     * `BroadcastStateFlow` now is deprecated |     * `BroadcastStateFlow` now is deprecated | ||||||
|   | |||||||
| @@ -1,10 +1,166 @@ | |||||||
|  | @file:Suppress("NOTHING_TO_INLINE") | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| fun <T> Iterable<T>.syncWith( | private inline fun <T> getObject( | ||||||
|     other: Iterable<T>, |     additional: MutableList<T>, | ||||||
|     removed: (List<T>) -> Unit = {}, |     iterator: Iterator<T> | ||||||
|     added: (List<T>) -> Unit = {} | ): T? = when { | ||||||
| ) { |     additional.isNotEmpty() -> additional.removeFirst() | ||||||
|     removed(filter { it !in other }) |     iterator.hasNext() -> iterator.next() | ||||||
|     added(other.filter { it !in this }) |     else -> null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Diff object which contains information about differences between two [Iterable]s | ||||||
|  |  * | ||||||
|  |  * @see calculateDiff | ||||||
|  |  */ | ||||||
|  | data class Diff<T> internal constructor( | ||||||
|  |     val removed: List<IndexedValue<T>>, | ||||||
|  |     /** | ||||||
|  |      * Old-New values pairs | ||||||
|  |      */ | ||||||
|  |     val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||||
|  |     val added: List<IndexedValue<T>> | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | private inline fun <T> performChanges( | ||||||
|  |     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||||
|  |     additionalsInOld: MutableList<T>, | ||||||
|  |     additionalsInNew: MutableList<T>, | ||||||
|  |     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 { | ||||||
|  |                     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!!) | ||||||
|  |                         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 <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) -> { | ||||||
|  |                 changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> }) | ||||||
|  |                 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 <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> | ||||||
|  | ) = calculateDiff(other, true) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare one-to-one | ||||||
|  |  */ | ||||||
|  | @Deprecated("Will be removed or replaced with some new function. Use calculateDiff instead") | ||||||
|  | inline fun <T> Iterable<T>.syncWith( | ||||||
|  |     other: Iterable<T>, | ||||||
|  |     noinline removed: (List<T>) -> Unit = {}, | ||||||
|  |     noinline added: (List<T>) -> Unit = {} | ||||||
|  | ) { | ||||||
|  |     calculateDiff(other).also { | ||||||
|  |         removed(it.removed.map { it.value }) | ||||||
|  |         added(it.added.map { it.value }) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user