diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4f3d85761..5965bb0e95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.18.1 + +* `Common`: + * Add `MapDiff` +* `Coroutines`: + * Add `SmartMutex` + ## 0.18.0 **ALL PREVIOUSLY DEPRECATED FUNCTIONALITY HAVE BEEN REMOVED** 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 167c94a21fd..4f2f2635ac4 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 @@ -200,20 +200,18 @@ inline fun Iterable.calculateStrictDiff( ) = calculateDiff(other, strictComparison = true) /** - * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] - * mutable list + * Applies [diff] to [this] [MutableList] */ fun MutableList.applyDiff( - source: Iterable, - strictComparison: Boolean = false -): Diff = calculateDiff(source, strictComparison).also { - for (i in it.removed.indices.sortedDescending()) { - removeAt(it.removed[i].index) + diff: Diff +) { + for (i in diff.removed.indices.sortedDescending()) { + removeAt(diff.removed[i].index) } - it.added.forEach { (i, t) -> + diff.added.forEach { (i, t) -> add(i, t) } - it.replaced.forEach { (_, new) -> + diff.replaced.forEach { (_, new) -> set(new.index, new.value) } } @@ -222,17 +220,30 @@ fun MutableList.applyDiff( * 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 { - for (i in it.removed.indices.sortedDescending()) { - removeAt(it.removed[i].index) - } - it.added.forEach { (i, t) -> - add(i, t) - } - it.replaced.forEach { (_, new) -> - set(new.index, new.value) - } + 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 +) diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt new file mode 100644 index 00000000000..dc275c347d1 --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt @@ -0,0 +1,135 @@ +package dev.inmo.micro_utils.common + +/** + * Contains diff based on the comparison of objects with the same [K]. + * + * @param removed Contains map with keys removed from parent map + * @param changed Contains map with keys values changed new map in comparison with old one + * @param added Contains map with new keys and values + */ +data class MapDiff @Warning(warning) constructor( + val removed: Map, + val changed: Map>, + val added: Map +) { + fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() + inline fun isNotEmpty() = !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 empty() = MapDiff(emptyMap(), emptyMap(), emptyMap()) + } +} + +private inline fun createCompareFun( + strictComparison: Boolean +): (K, V, V) -> Boolean = if (strictComparison) { + { _, first, second -> first === second } +} else { + { _, first, second -> first == second } +} + +/** + * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] + * + * @param compareFun Will be used to determine changed values + */ +fun Map.diff( + other: Map, + compareFun: (K, V, V) -> Boolean +): MapDiff { + val removed: Map = (keys - other.keys).associateWith { + getValue(it) + } + val added: Map = (other.keys - keys).associateWith { + other.getValue(it) + } + val changed = keys.intersect(other.keys).mapNotNull { + val old = getValue(it) + val new = other.getValue(it) + if (compareFun(it, old, new)) { + return@mapNotNull null + } else { + it to (old to new) + } + }.toMap() + + return MapDiff( + removed, + changed, + added + ) +} + +/** + * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] + * + * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard + * `equals` will be used + */ +fun Map.diff( + other: Map, + strictComparison: Boolean = false +): MapDiff = diff( + other, + compareFun = createCompareFun(strictComparison) +) + +/** + * Will apply [mapDiff] to [this] [MutableMap] + */ +fun MutableMap.applyDiff( + mapDiff: MapDiff +) { + mapDiff.apply { + removed.keys.forEach { remove(it) } + changed.forEach { (k, oldNew) -> + put(k, oldNew.second) + } + added.forEach { (k, new) -> + put(k, new) + } + } +} + +/** + * Will apply changes with [from] map into [this] one + * + * @param compareFun Will be used to determine changed values + * + * @return [MapDiff] applied to [this] [MutableMap] + */ +fun MutableMap.applyDiff( + from: Map, + compareFun: (K, V, V) -> Boolean +): MapDiff { + return diff(from, compareFun).also { + applyDiff(it) + } +} + +/** + * Will apply changes with [from] map into [this] one + * + * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard + * `equals` will be used + * + * @return [MapDiff] applied to [this] [MutableMap] + */ +fun MutableMap.applyDiff( + from: Map, + strictComparison: Boolean = false +): MapDiff = applyDiff( + from, + compareFun = createCompareFun(strictComparison) +) + +/** + * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the + * [MapDiff.changed] values will be reversed too + */ +fun MapDiff.reversed(): MapDiff = MapDiff( + removed = added, + changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, + added = removed +) diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartMutex.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartMutex.kt new file mode 100644 index 00000000000..c8a962d453c --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartMutex.kt @@ -0,0 +1,136 @@ +package dev.inmo.micro_utils.coroutines + +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.isActive +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the + * [SmartMutex] state. + * + * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need + * [Mutable] [SmartMutex]. Otherwise, [Immutable]. + * + * Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its + * [Mutable] creator + */ +sealed interface SmartMutex { + val lockStateFlow: StateFlow + + /** + * * True - locked + * * False - unlocked + */ + val isLocked: Boolean + get() = lockStateFlow.value + + /** + * Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow] + */ + class Immutable(override val lockStateFlow: StateFlow) : SmartMutex + + /** + * Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create + * [Immutable] variant of [this] instance with [immutable] factory + * + * @param locked Preset state of [isLocked] and its internal [_lockStateFlow] + */ + class Mutable(locked: Boolean = false) : SmartMutex { + private val _lockStateFlow = MutableStateFlow(locked) + override val lockStateFlow: StateFlow = _lockStateFlow.asStateFlow() + + private val internalChangesMutex = Mutex() + + fun immutable() = Immutable(lockStateFlow) + + /** + * Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will + * wait for [isLocked] == false and then try to lock + */ + suspend fun lock() { + do { + waitUnlock() + val shouldContinue = internalChangesMutex.withLock { + if (_lockStateFlow.value) { + true + } else { + _lockStateFlow.value = true + false + } + } + } while (shouldContinue && currentCoroutineContext().isActive) + } + + /** + * Will try to lock this [SmartMutex] immediataly + * + * @return True if lock was successful. False otherwise + */ + suspend fun tryLock(): Boolean { + return if (!_lockStateFlow.value) { + internalChangesMutex.withLock { + if (!_lockStateFlow.value) { + _lockStateFlow.value = true + true + } else { + false + } + } + } else { + false + } + } + + /** + * If [isLocked] == true - will change it to false and return true. If current call will not unlock this + * [SmartMutex] - false + */ + suspend fun unlock(): Boolean { + return if (_lockStateFlow.value) { + internalChangesMutex.withLock { + if (_lockStateFlow.value) { + _lockStateFlow.value = false + true + } else { + false + } + } + } else { + false + } + } + } +} + +/** + * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun SmartMutex.Mutable.withLock(action: () -> T): T { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + lock() + try { + return action() + } finally { + unlock() + } +} + +/** + * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. + * + * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact + * that some other parties may lock it again + */ +suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it } diff --git a/gradle.properties b/gradle.properties index 5db53e2a94f..9f2f9380401 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.18.0 -android_code_version=191 +version=0.18.1 +android_code_version=192