diff --git a/CHANGELOG.md b/CHANGELOG.md index c94ef7aaeeb..85a096e0561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.2.4 + +* `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 + * New extensions for `Flow`s: + * `Flow#subscribe` + * `Flow#subscribeSafely` + * `Flow#subscribeSafelyWithoutExceptions` + ## 0.2.3 * `Versions` 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..7033edcfb6f 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, strictComparison = 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..6bf75aa9dd4 --- /dev/null +++ b/common/src/commonTest/kotlin/dev/inmo/micro_utils/common/DiffUtilsTests.kt @@ -0,0 +1,76 @@ +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.calculateDiff(oldList - removedSublist).apply { + assertEquals( + removedSublist.mapIndexed { j, o -> IndexedValue(i + j, o) }, + removed + ) + } + } + } + } + + @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).map { "added$it" } + val mutable = oldList.toMutableList() + mutable.addAll(i, addedSublist) + oldList.calculateDiff(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.calculateDiff(mutable).apply { + assertEquals( + changes, + replaced + ) + } + } + } + } +} diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastFlow.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastFlow.kt index d3ff5b0b277..f4c18906a82 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastFlow.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastFlow.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* @Suppress("FunctionName") +@Deprecated("Deprecated due to stabilization of SharedFlow and StateFlow") fun BroadcastFlow( internalChannelSize: Int = Channel.BUFFERED ): BroadcastFlow { @@ -15,6 +16,7 @@ fun BroadcastFlow( ) } +@Deprecated("Deprecated due to stabilization of SharedFlow and StateFlow") class BroadcastFlow internal constructor( private val channel: BroadcastChannel, private val flow: Flow diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastStateFlow.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastStateFlow.kt index e2ad31a4af3..53d29ae713f 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastStateFlow.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/BroadcastStateFlow.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.* const val defaultBroadcastStateFlowReplayCacheSize = 1 +@Deprecated("Deprecated due to stabilization of SharedFlow and StateFlow") class BroadcastStateFlow internal constructor( parentFlow: Flow, initial: T, @@ -34,17 +35,20 @@ class BroadcastStateFlow internal constructor( } } +@Deprecated("Deprecated due to stabilization of SharedFlow and StateFlow") fun BroadcastChannel.asStateFlow( value: T, scope: CoroutineScope, replayCacheSize: Int = defaultBroadcastStateFlowReplayCacheSize ): StateFlow = BroadcastStateFlow(asFlow(), value, replayCacheSize, scope) +@Deprecated("Deprecated due to stabilization of SharedFlow and StateFlow") fun BroadcastChannel.asStateFlow( scope: CoroutineScope, replayCacheSize: Int = defaultBroadcastStateFlowReplayCacheSize ): StateFlow = asStateFlow(null, scope, replayCacheSize) +@Deprecated("Deprecated due to stabilization of SharedFlow and StateFlow") fun broadcastStateFlow( initial: T, scope: CoroutineScope, channelSize: Int = Channel.BUFFERED, @@ -55,6 +59,7 @@ fun broadcastStateFlow( it to it.asStateFlow(initial, scope, replayCacheSize) } +@Deprecated("Deprecated due to stabilization of SharedFlow and StateFlow") fun broadcastStateFlow( scope: CoroutineScope, channelSize: Int = Channel.BUFFERED, diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/FlowSubscription.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/FlowSubscription.kt new file mode 100644 index 00000000000..474c230f2c0 --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/FlowSubscription.kt @@ -0,0 +1,37 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package dev.inmo.micro_utils.coroutines + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +/** + * Shortcut for chain if [Flow.onEach] and [Flow.launchIn] + */ +inline fun Flow.subscribe(scope: CoroutineScope, noinline block: suspend (T) -> Unit) = onEach(block).launchIn(scope) + +/** + * Use [subscribe], but all [block]s will be called inside of [safely] function. + * Use [onException] to set up your reaction for [Throwable]s + */ +inline fun Flow.subscribeSafely( + scope: CoroutineScope, + noinline onException: ExceptionHandler = { throw it }, + noinline block: suspend (T) -> Unit +) = subscribe(scope) { + safely(onException) { + block(it) + } +} + +/** + * Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped + */ +inline fun Flow.subscribeSafelyWithoutExceptions( + scope: CoroutineScope, + noinline block: suspend (T) -> Unit +) = subscribeSafely( + scope, + {}, + block +) diff --git a/gradle.properties b/gradle.properties index 06be361b781..12a32a9606a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ kotlin.incremental.js=true kotlin_version=1.4.10 kotlin_coroutines_version=1.4.0 -kotlin_serialisation_core_version=1.0.0 +kotlin_serialisation_core_version=1.0.1 kotlin_exposed_version=0.28.1 ktor_version=1.4.1 @@ -19,4 +19,4 @@ github_release_plugin_version=2.2.12 uuidVersion=0.2.2 group=dev.inmo -version=0.2.3 +version=0.2.4