+added more useful collections
This commit is contained in:
parent
d29bf960aa
commit
f6d2422cc1
@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.1.9-SNAPSHOT"
|
version = "0.1.10-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@ -51,7 +51,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
|
||||||
// this is actually a bug: we need only the core, but bare core causes strange errors
|
// this is actually a bug: we need only the core, but bare core causes strange errors
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
||||||
api("net.sergeych:mp_stools:[1.4.7,)")
|
api("net.sergeych:mp_stools:[1.4.7,)")
|
||||||
@ -73,6 +73,8 @@ kotlin {
|
|||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jvmMain by getting
|
val jvmMain by getting
|
||||||
@ -87,7 +89,11 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val wasmJsTest by getting
|
val wasmJsTest by getting {
|
||||||
|
dependencies {
|
||||||
|
// implementation("org.jetbrains.kotlinx:kotlinx-browser:0.3")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
41
gradlew
vendored
41
gradlew
vendored
@ -55,7 +55,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@ -80,11 +80,13 @@ do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# This is normally unused
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
# shellcheck disable=SC2034
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@ -131,29 +133,22 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
if ! command -v java >/dev/null 2>&1
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@ -198,15 +193,11 @@ if "$cygwin" || "$msys" ; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
# Collect all arguments for the java command:
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
@ -214,12 +205,6 @@ set -- \
|
|||||||
org.gradle.wrapper.GradleWrapperMain \
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
# Use "xargs" to parse quoted args.
|
||||||
#
|
#
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
11
gradlew.bat
vendored
11
gradlew.bat
vendored
@ -26,7 +26,6 @@ if "%OS%"=="Windows_NT" setlocal
|
|||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
exit /b 1
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
@ -8,6 +8,7 @@ package net.sergeych.bintools
|
|||||||
*
|
*
|
||||||
* Note that the cost, [MRUCache] is slower than [MutableMap].
|
* 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,
|
class MRUCache<K,V>(val maxSize: Int,
|
||||||
private val cache: LinkedHashMap<K,V> = LinkedHashMap()
|
private val cache: LinkedHashMap<K,V> = LinkedHashMap()
|
||||||
): MutableMap<K,V> by cache {
|
): 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
|
||||||
|
}
|
||||||
|
|
64
src/commonMain/kotlin/net/sergeych/collections/MRUCache.kt
Normal file
64
src/commonMain/kotlin/net/sergeych/collections/MRUCache.kt
Normal file
@ -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()
|
||||||
|
}
|
199
src/commonMain/kotlin/net/sergeych/collections/SortedList.kt
Normal file
199
src/commonMain/kotlin/net/sergeych/collections/SortedList.kt
Normal file
@ -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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
81
src/commonTest/kotlin/collections/CollectionsTest.kt
Normal file
81
src/commonTest/kotlin/collections/CollectionsTest.kt
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package collections
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import net.sergeych.collections.ExpirableAsyncCache
|
||||||
|
import net.sergeych.collections.SortedList
|
||||||
|
import kotlin.test.*
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class CollectionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSortedList1() {
|
||||||
|
val a1 = SortedList(5,4,3,9,1)
|
||||||
|
assertTrue { 4 in a1 }
|
||||||
|
assertTrue { 14 !in a1 }
|
||||||
|
fun <T: Comparable<T>>test(x: SortedList<T>) {
|
||||||
|
var last: T? = null
|
||||||
|
for (i in x.toList()) {
|
||||||
|
if( last == null ) last = i
|
||||||
|
else if( last > i ) fail("invalid order: $last should be <= $i")
|
||||||
|
assertContains(x, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test(a1)
|
||||||
|
a1.add(11, 3, 2, 1, 0, 9, 22, -55, 0, 1, 0)
|
||||||
|
println(a1.toList())
|
||||||
|
assertEquals(listOf(-55, 0, 0, 0, 1, 1, 1, 2, 3, 3, 4, 5, 9, 9, 11, 22), a1.toList())
|
||||||
|
assertEquals(11, a1.find(11))
|
||||||
|
assertEquals(listOf(0,0,0), a1.findAll(0))
|
||||||
|
assertEquals(listOf(11), a1.findAll(11))
|
||||||
|
assertEquals(listOf(3,3), a1.findAll(3))
|
||||||
|
assertTrue { a1.remove(3) }
|
||||||
|
assertEquals(listOf(3), a1.findAll(3))
|
||||||
|
assertTrue { a1.remove(3) }
|
||||||
|
assertEquals(listOf(), a1.findAll(3))
|
||||||
|
assertTrue { 3 !in a1 }
|
||||||
|
assertEquals(3, a1.findAll(1).size)
|
||||||
|
assertEquals( 3, a1.removeAll(1))
|
||||||
|
assertTrue { 1 !in a1 }
|
||||||
|
assertEquals(listOf(-55, 0, 0, 0, 2, 4, 5, 9, 9, 11, 22), a1.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun expirableAsyncCacheTest() = runTest {
|
||||||
|
val removedValues = mutableSetOf<Int>()
|
||||||
|
val m = ExpirableAsyncCache<String,Int>(500.milliseconds) {
|
||||||
|
removedValues += it
|
||||||
|
}
|
||||||
|
m.put("one", 1)
|
||||||
|
m.put("two", 2)
|
||||||
|
assertTrue("one" in m)
|
||||||
|
assertTrue("two" in m)
|
||||||
|
assertEquals(1, m.get("one"))
|
||||||
|
assertEquals(2, m.get("two"))
|
||||||
|
assertTrue { removedValues.isEmpty() }
|
||||||
|
|
||||||
|
m.put("one", 11)
|
||||||
|
assertEquals(11, m.get("one"))
|
||||||
|
assertEquals(removedValues, setOf(1))
|
||||||
|
|
||||||
|
m.getOrDefault("two", 22)
|
||||||
|
assertEquals(2, m.get("two"))
|
||||||
|
|
||||||
|
m.getOrPut("two") {222}
|
||||||
|
assertEquals(2, m.get("two"))
|
||||||
|
|
||||||
|
m.getOrPut("three") { 3 }
|
||||||
|
assertEquals(3, m.get("three"))
|
||||||
|
// This sadly is not working
|
||||||
|
// delay(2000)
|
||||||
|
// advanceTimeBy(2000)
|
||||||
|
// delay(1000)
|
||||||
|
// assertNull(m.get("one"))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user