Skip to content

Commit

Permalink
simple lru cache implementation
Browse files Browse the repository at this point in the history
AbdullinAM committed Aug 2, 2023
1 parent bcc4ab7 commit 922e350
Showing 3 changed files with 205 additions and 1 deletion.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
<artifactId>kt-helper</artifactId>
<groupId>org.vorpal.research</groupId>
<packaging>jar</packaging>
<version>0.1.11</version>
<version>0.1.12</version>

<properties>
<jvm.version>1.8</jvm.version>
152 changes: 152 additions & 0 deletions src/main/kotlin/org/vorpal/research/kthelper/collection/cache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package org.vorpal.research.kthelper.collection

import org.vorpal.research.kthelper.assert.ktassert

interface Cache<K, V> {
val maxSize: UInt
val size: UInt
operator fun set(key: K, value: V)
operator fun get(key: K): V?
operator fun contains(key: K): Boolean
fun clear()
}


@Suppress("unused")
private class DoublyLinkedList<T> {
var head: Node? = null
var tail: Node? = null

inner class Node(var item: T) {
var previous: Node? = null
var next: Node? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@Suppress("UNCHECKED_CAST")
other as DoublyLinkedList<T>.Node
return item == other.item
}

override fun hashCode(): Int {
return item?.hashCode() ?: 0
}
}

fun add(element: T): Node {
val node = Node(element)
if (head == null) {
head = node
} else if (tail == null) {
tail = node
head!!.next = tail
tail!!.previous = head
} else {
tail!!.next = node
node.previous = tail
tail = node
}
return node
}

fun moveToFront(node: Node) {
if (node == head) return
val temp = head!!
head!!.item = node.item
head!!.previous = node.previous
head!!.next = node.next

node.item = temp.item
node.previous = temp.previous
node.next = temp.next
}

fun clear() {
var current = head
while (current != null) {
val next = current.next
current.previous = null
current.next = null
current = next
}
head = null
tail = null
}

fun removeFirst(): Node? {
return when {
head == null -> null
tail == null -> head.also {
head = null
}
else -> tail.also {
val next = head!!.next
next!!.previous = null
tail!!.previous = null
head!!.next = null
head = next
}
}
}

fun removeLast(): Node? {
return when {
head == null -> null
tail == null -> head.also {
head = null
}
tail?.previous == head -> tail.also {
head!!.next = null
tail!!.previous = null
tail = null
}
else -> tail.also {
val previous = tail!!.previous
previous!!.next = null
tail!!.previous = null
tail!!.next = null
tail = previous
}
}
}
}

class LRUCache<K, V>(override val maxSize: UInt) : Cache<K, V> {
val cache = HashMap<K, V>()
private val query = DoublyLinkedList<K>()
private val nodeMap = HashMap<K, DoublyLinkedList<K>.Node>()

override val size: UInt get() = cache.size.toUInt()

init {
ktassert(maxSize > 0U, "Cache should have positive size")
}

override fun set(key: K, value: V) {
val shouldRemove = key !in cache
while (shouldRemove && cache.size >= maxSize.toInt()) {
val last = query.removeLast() ?: break
nodeMap.remove(last.item)
cache.remove(last.item)
}

val node = nodeMap.getOrPut(key) { query.add(key) }
cache[key] = value
query.moveToFront(node)
}

override fun get(key: K): V? {
return cache[key]
}

override fun contains(key: K): Boolean {
return key in cache
}


override fun clear() {
cache.clear()
query.clear()
nodeMap.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.vorpal.research.kthelper.collection

import org.junit.Assert.*
import org.junit.Test
import org.vorpal.research.kthelper.KtException
import kotlin.random.Random

class LRUCacheTest {
@Test
fun testCache() {
assertThrows(KtException::class.java) { LRUCache<Int, Int>(0U) }

val sizes = listOf(1U, 100U, 10_000U, 100_000U)
for (size in sizes) {
val cache = LRUCache<Int, Int>(size)
val currentValues = HashMap<Int, Int>()
val random = Random(size.toInt())

repeat(1_000_000) {
val next = random.nextInt()
val value = random.nextInt()
when (next) {
in currentValues -> {
if (next in cache) {
assertEquals(currentValues[next], cache[next])
}

val oldSize = cache.size
cache[next] = value
currentValues[next] = value
assertTrue(oldSize <= size)
assertEquals(currentValues[next], cache[next])
assertEquals(oldSize, cache.size)
}
else -> {
assertFalse(next in cache)
val oldSize = cache.size

cache[next] = value
currentValues[next] = value
if (oldSize < size) {
assertEquals(oldSize + 1U, cache.size)
} else {
assertEquals(oldSize, cache.size)
}
assertEquals(currentValues[next], cache[next])
}
}
}
}
}
}

0 comments on commit 922e350

Please sign in to comment.