+added more useful collections

This commit is contained in:
sergeych committed 2024-12-22 12:22:51 +03:00
1 parent d29bf960aa
commit f6d2422cc1
8 files changed
+541 -40

No files matched your search

@@ -8,6 +8,7 @@ package net.sergeych.bintools
*
* Note that the cost, [MRUCache] is slower than [MutableMap].
*/
@Deprecated("moved to net.sergeych.collections", ReplaceWith("net.sergeych.collections.MRUCache"))
class MRUCache<K,V>(val maxSize: Int,
private val cache: LinkedHashMap<K,V> = LinkedHashMap()
): MutableMap<K,V> by cache {
@@ -0,0 +1,168 @@
package net.sergeych.collections
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import net.sergeych.mptools.withReentrantLock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* MRU cache with expiration, with safe async concurrent access.
*
* Expired items are removed when accessing the map, when reading values or when putting
* it when [maxCapacity] is reached. See note about freeing resources below.
*
* It is much like [Map] and [MutableMap], but using suspend functions now limit usage of
* operator functions so we are not implementing it. Also, modification with [entries] is
* not allowed.
*
* Unlike [MRUCache], it drops expired values. Removing expired item is lazy, actual resource
* freeing could be delayed. To force actual removal use [cleanup].
*
* @param lifeTime how long the value should be kept
* @param maxCapacity if set, limits the capacity. Least Recent Used elements would be dropped
* to fit this parameter.
* @param onItemRemoved called when some item is removed for any reason (e.g. expiration or overwriting).
* Note that this call also suspends put variants until done
*/
class ExpirableAsyncCache<K, V>(
val lifeTime: Duration = 30.seconds,
val maxCapacity: Int? = null,
val onItemRemoved: (suspend (V) -> Unit)? = null
) {
class Slot<V>(
var value: V,
var lastUsedAt: Instant = Clock.System.now(),
)
private val access = Mutex()
private val cache = mutableMapOf<K, Slot<V>>()
suspend fun get(key: K): V? {
return access.withReentrantLock {
cache.get(key)?.let {
val now = Clock.System.now()
println("lifetime $key: ${now - it.lastUsedAt}")
if (now - it.lastUsedAt > lifeTime) {
cache.remove(key)
onItemRemoved?.invoke(it.value)
null
} else {
it.lastUsedAt = now
it.value
}
}
}
}
/**
* Put the value for key. Calls [onItemRemoved] if needed.
* @return previous value or null
*/
suspend fun put(key: K, value: V): V? {
// insert may replace existing item, so we do it first:
return access.withLock {
cache[key]?.let {
if (value != it.value)
onItemRemoved?.invoke(it.value)
val oldValue = it.value
it.value = value
it.lastUsedAt = Clock.System.now()
oldValue
} ?: run {
// overflow could be caused by put, so put first
cache.put(key, Slot(value))
// now check size
fixSize()
null
}
}
}
private suspend fun fixSize() {
maxCapacity?.let {
if (it >= cache.size) {
cache.remove(cache.minBy { it.value.lastUsedAt }.key)
?.also { onItemRemoved?.invoke(it.value) }
?.value
}
}
}
/**
* Remove all expired elements. This function is not needed unless you
* want to free resources associated with expired elements immediately.
*
* [onItemRemoved] is called for each removed item before returning.
*/
@Suppress("unused")
suspend fun cleanup() {
access.withLock {
val d = Clock.System.now()
for( e in cache.entries.toList()) {
if( d - e.value.lastUsedAt > lifeTime )
cache.remove(e.key)
}
}
}
suspend fun getOrDefault(key: K, value: V): V = get(key) ?: value
/**
* Atomically get or put value to the cache.
*
* If there is expired existing value, [onItemRemoved] will be called for it
* before assigning new value.
*/
suspend fun getOrPut(key: K, defaultValue: suspend () -> V): V {
return access.withLock {
cache[key]?.let {
if( Clock.System.now() - it.lastUsedAt > lifeTime) {
onItemRemoved?.invoke(it.value)
}
it.lastUsedAt = Clock.System.now()
it.value
} ?: run {
val v = defaultValue()
cache[key] = Slot(v)
fixSize()
v
}
}
}
data class Entry<K, V>(override val key: K, override val value: V) : Map.Entry<K, V>
val entries: Set<Map.Entry<K, V>>
get() = cache.entries.map { Entry(it.key, it.value.value) }.toSet()
val keys: Set<K>
get() = cache.keys
val size: Int
get() = cache.size
@Suppress("unused")
val values: Collection<V>
get() = cache.values.map { it.value }
@Suppress("unused")
fun isEmpty(): Boolean = cache.isEmpty()
@Suppress("unused")
fun containsValue(value: V): Boolean {
for (v in cache.values)
if (v.value == value) return true
return false
}
@Suppress("unused")
fun containsKey(key: K): Boolean = key in cache
operator fun contains(k: K): Boolean = k in cache
}
@@ -0,0 +1,64 @@
package net.sergeych.collections
/**
* Most Recently Used keys Cache.
* Maintains the specified size, removed least used elements on insertion. Element usage is
* when it is inserted, updated or accessed (with [get]). Least recently used (LRU) keys
* are automatically removed to maintain the [maxSize].
*
* Note that the cost, [MRUCache] is slower than [MutableMap].
*/
class MRUCache<K,V>(val maxSize: Int,
private val cache: LinkedHashMap<K,V> = LinkedHashMap()
): MutableMap<K,V> by cache {
private fun checkSize() {
while(cache.size > maxSize) {
cache.remove(cache.keys.first())
}
}
/**
* Put the [value] associated with [key] which becomes MRU whether it existed in the cache or was added now.
*
* If [size] == [maxSize] LRU key will be dropped.
*
* @return old value for the [key] or null
*/
override fun put(key: K, value: V): V? {
// we need it to become MRU, so we remove it to clear its position
val oldValue = cache.remove(key)
// now we always add, not update, so it will become MRU element:
cache.put(key,value).also { checkSize() }
return oldValue
}
/**
* Put all the key-value pairs, this is exactly same as calling [put] in the same
* order. Note that is the [from] map is not linked and its size is greater than
* [maxSize], some unpredictable keys will not be added. To be exact, only last
* [maxSize] keys will be added by the order providing by [from] map entries
* enumerator.
*
* If from is [LinkedHashMap] or like, onl
*/
override fun putAll(from: Map<out K, V>) {
// maybe we should optimize it not to add unnecessary first keys
for( e in from) {
put(e.key,e.value)
checkSize()
}
}
/**
* Get the value associated with the [key]. It makes the [key] a MRU (last to delete)
*/
override fun get(key: K): V? {
return cache[key]?.also {
cache.remove(key)
cache[key] = it
}
}
override fun toString(): String = cache.toString()
}
@@ -0,0 +1,199 @@
package net.sergeych.collections
/**
* Automatically mutable sorted list based on binary search and a given comparing function.
* To construct list of comparable elements use [invoke]. There is a secondary constructor
* to use with existing [Comparator] instance.
*
* While the sorted list is mutable, it does not implement `MutableList` because indexed
* assignments are not possible keeping sort order; use [add] instead.
*
* It is possible to store several equal values and retrieve them all. See [add] and [addIfNotExists].
*/
class SortedList<T: Any>(
private val list: MutableList<T> = mutableListOf(),
private val compare: (T,T) -> Int
)
: List<T> by list
{
@Suppress("unused")
constructor(list: MutableList<T>, comparator: Comparator<T>)
: this(list,{ a, b -> comparator.compare(a,b) })
private fun binarySearch(element: T): Int {
var low = 0
var high = this.size - 1
while (low <= high) {
val mid = (low + high).ushr(1) // unsigned shift right, equivalent to integer division of sum by 2
val midVal = list[mid]
val cmp = compare(element,midVal)
when {
cmp < 0 -> high = mid - 1
cmp > 0 -> low = mid + 1
else -> return mid // key found
}
}
return -(low + 1) // key not found, insertion point is -(low + 1)
}
/**
* Find any element equals to value using fast binary search.
*
* Note that if there are many elements that are equal to [value] using the [compare],
* it will return index of some of it. Use [findFirst] and [findLast] if needed.
*
* @return found value or null
*/
fun find(value: T): T? {
val i = binarySearch(value)
return if( i < 0 ) null else list[i]
}
/**
* Find all elements equal to the value.
* @return list of found elements, or an empty list.
*/
fun findAll(value: T): List<T> {
val result = mutableListOf<T>()
val start = binarySearch(value)
if( start >= 0) {
for( i in start ..< size ) {
val element = list[i]
if( compare(value, element) == 0 )
result += element
else
break
}
if( start > 0) {
for( i in (start-1) downTo 0) {
val element = list[i]
if (compare(value, element) == 0)
result += element
else
break
}
}
}
return result
}
/**
* Add all values. Duplicates will also be added.
*/
fun add(vararg values: T) {
for( value in values ) {
val i = binarySearch(value)
if (i >= 0)
list.add(i + 1, value)
else
list.add(-(i + 1), value)
}
}
/**
* Remove one element equals to value.
* @return true if the element has been removed
*/
fun remove(value: T): Boolean {
val i = binarySearch(value)
return if( i >= 0) {
list.removeAt(i)
true
}
else false
}
/**
* Remove element at index.
* @return element that has been removed
*/
@Suppress("unused")
fun removeAt(index: Int): T = list.removeAt(index)
/**
* Optimized, binary search based version.
* @returns index of the _first_ occurrence of the element, or -1
*/
override fun indexOf(element: T): Int {
var i = binarySearch(element)
if( i < 0 ) return -1
while( i > 0 && compare(element, list[i-1]) == 0) i--
return i
}
/**
* Optimized, binary search based version.
* @returns index of the _last_ occurrence of the element, or -1
*/
override fun lastIndexOf(element: T): Int {
var i = binarySearch(element)
if( i < 0 ) return -1
while( i < list.size && compare(element, list[i+1])==0) i++
return i
}
/**
* Optimized, binary search based, first occurrence of the element.
*
* Note that the order of 'equal' elements is unspecified, order of appearance is not kept.
*/
fun findFirst(element: T): T? {
val i = indexOf(element)
return if( i < 0 ) null else list[i]
}
/**
* Optimized, binary search based search of the last occurrence of the element
*
* Note that the order of 'equal' elements is unspecified, order of appearance is not kept.
*/
fun findLast(element: T): T? {
val i = lastIndexOf(element)
return if( i < 0 ) null else list[i]
}
/**
* Remove all elements equals to value.
* @return number of removed elements
*/
fun removeAll(value: T): Int {
var start = binarySearch(value)
var count = 0
while( start < size && compare(value, list[start]) == 0 ) {
list.removeAt(start)
count++
}
while( start > 0 && compare(value, list[--start]) == 0) {
list.removeAt(start)
count++
}
return count
}
override operator fun contains(element: T): Boolean = binarySearch(element) >= 0
/**
* Add a value if it is not yet in the list.
* @return true if the value was added and false if it is already in the list, and was not added.
*/
fun addIfNotExists(value: T): Boolean {
val i = binarySearch(value)
return if( i < 0) {
list.add(-(i + 1), value)
true
}
else false
}
companion object {
/**
* Construct list of elements from comparable instances.
*/
operator fun <T: Comparable<T>>invoke(vararg values: T): SortedList<T> =
SortedList(values.toList().sorted().toMutableList()) { a, b -> a.compareTo(b) }
}
}