diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c74e713a2d..5965bb0e95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 0.18.1 +* `Common`: + * Add `MapDiff` * `Coroutines`: * Add `SmartMutex` 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..3f0e43adb4c --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/MapDiff.kt @@ -0,0 +1,110 @@ +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 changes with [other] map into [this] one + * + * @param compareFun Will be used to determine changed values + */ +fun MutableMap.applyDiff( + from: Map, + compareFun: (K, V, V) -> Boolean = { _, first, second -> first == second } +) { + diff(from, compareFun).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 [other] map into [this] one + * + * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard + * `equals` will be used + */ +fun MutableMap.applyDiff( + other: Map, + strictComparison: Boolean = false +) = applyDiff( + other, + compareFun = createCompareFun(strictComparison) +)