From bc6f9860141769251d8db409d514ed0c4963413f Mon Sep 17 00:00:00 2001 From: Jayson Minard Date: Mon, 25 Nov 2024 18:28:36 -0600 Subject: [PATCH] fixes for AdvancedLRU with naive implementtion, LRUCache, and AnyCallback (#144) --- .../igorwojda/cache/advancedlru/Challenge.kt | 53 ++------ .../com/igorwojda/cache/advancedlru/README.md | 29 +++-- .../igorwojda/cache/advancedlru/Solution.kt | 112 +++++++++------- .../com/igorwojda/cache/advancedlru/Tests.kt | 120 ++++++++++++++++++ .../com/igorwojda/cache/lru/Challenge.kt | 52 ++------ .../kotlin/com/igorwojda/cache/lru/README.md | 2 +- .../com/igorwojda/cache/lru/Solution.kt | 61 ++++----- .../kotlin/com/igorwojda/cache/lru/Tests.kt | 43 +++++++ .../igorwojda/common/anycallback/Challenge.kt | 17 +-- .../igorwojda/common/anycallback/README.md | 2 +- .../igorwojda/common/anycallback/Solution.kt | 37 +++++- .../com/igorwojda/common/anycallback/Tests.kt | 27 ++++ 12 files changed, 362 insertions(+), 193 deletions(-) create mode 100644 src/test/kotlin/com/igorwojda/cache/advancedlru/Tests.kt create mode 100644 src/test/kotlin/com/igorwojda/cache/lru/Tests.kt create mode 100644 src/test/kotlin/com/igorwojda/common/anycallback/Tests.kt diff --git a/src/test/kotlin/com/igorwojda/cache/advancedlru/Challenge.kt b/src/test/kotlin/com/igorwojda/cache/advancedlru/Challenge.kt index a00acba8..46358b64 100644 --- a/src/test/kotlin/com/igorwojda/cache/advancedlru/Challenge.kt +++ b/src/test/kotlin/com/igorwojda/cache/advancedlru/Challenge.kt @@ -1,52 +1,19 @@ package com.igorwojda.cache.advancedlru -import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Test -import java.util.* +import java.time.Clock +import java.time.Duration -class AdvancedLRUCache(private val capacity: Int) { - fun put(key: String, value: Int, priority: Int, expiryTime: Long) { - TODO("Add your solution here") - } - - fun get(key: String): Int? { - TODO("Add your solution here") - } - - // Returns fixed system time in milliseconds - private fun getSystemTimeForExpiry() = 1000 +interface LRUCache { + fun put(key: K, value: V, priority: Int, ttl: Duration) + fun get(key: K): V? } -private class Test { - @Test - fun `add and get`() { - val cache = AdvancedLRUCache(2) - cache.put("A", 1, 5, 5000) - - cache.get("A") shouldBeEqualTo 1 - } - - @Test - fun `evict by priority`() { - val cache = AdvancedLRUCache(2) - cache.put("A", 1, 1, 3000) - cache.put("B", 2, 3, 4000) - cache.put("C", 3, 4, 5000) - - // This should be null because "A" was evicted due to lower priority. - cache.get("A") shouldBeEqualTo null - cache.get("B") shouldBeEqualTo 2 - cache.get("C") shouldBeEqualTo 3 +class AdvancedLRUCache(private val capacity: Int, private val clock: Clock = Clock.systemDefaultZone()): LRUCache { + override fun put(key: K, value: V, priority: Int, ttl: Duration) { + TODO("Add your solution here") } - @Test - fun `evict by expiry`() { - val cache = AdvancedLRUCache(2) - cache.put("A", 1, 1, 500) - cache.put("B", 2, 3, 700) - - // This should be null because "A" was evicted due to expiry. - cache.get("A") shouldBeEqualTo null - cache.get("B") shouldBeEqualTo null + override fun get(key: K): V? { + TODO("Add your solution here") } } diff --git a/src/test/kotlin/com/igorwojda/cache/advancedlru/README.md b/src/test/kotlin/com/igorwojda/cache/advancedlru/README.md index f6e877a4..6aa09b13 100644 --- a/src/test/kotlin/com/igorwojda/cache/advancedlru/README.md +++ b/src/test/kotlin/com/igorwojda/cache/advancedlru/README.md @@ -11,38 +11,39 @@ limit. In cases where the addition of new items exceeds this capacity, ensure th following sequence of operations: - Firstly, discard items that have exceeded their validity period (`expiryTime` > `getSystemTimeForExpiry()`). -- If there are no items past their validity, identify the items with the lowest priority rating and from these, remove +- If there are no items past their validity, identify the items with the earliest expiry time, and from those the items with the lowest priority rating, and from these remove the item that was least recently accessed or used. -To simplify expiry logic testing use provided `getSystemTime()` method (instead of `System.currentTimeMillis()`) that -will return fixed system time in milliseconds. +To simplify expiry logic testing use the provided `Clock` to determine the current time in milliseconds using `clock.millis()`. -[Challenge](Challenge.kt) | [Solution](Solution.kt) +[Challenge](Challenge.kt) | [Solution](Solution.kt) | [Tests](Tests.kt) ## Examples ```kotlin val cache = AdvancedLRUCache(2) -cache.put("A", 1, 5, 5000) +cache.put("A", 1, 5, Duration.ofMinutes(15)) cache.get("A") // 1 ``` ```kotlin -val cache = AdvancedLRUCache(2) -cache.put("A", 1, 1, 3000) -cache.put("B", 2, 3, 4000) -cache.put("C", 3, 4, 5000) +val cache = AdvancedLRUCache(2, Clock.fixed(...)) // testing clock, fixed at a moment in time +cache.put("A", 1, 5, Duration.ofMinutes(15)) +cache.put("B", 2, 1, Duration.ofMinutes(15)) +cache.put("C", 3, 10, Duration.ofMinutes(15)) -cache.get("A") // null - "A" was evicted due to lower priority. -cache.get("B") // 2 +cache.get("A") // 1 +cache.get("B") // null - "B" was evicted due to lower priority. cache.get("C") // 3 ``` ```kotlin -val cache = AdvancedLRUCache(2) -cache.put("A", 1, 1, 500) -cache.put("B", 2, 3, 700) +val cache = AdvancedLRUCache(100) +cache.put("A", 1, 1, Duration.ofMillis(1)) +cache.put("B", 2, 1, Duration.ofMillis(1)) + +sleep(100) cache.get("A") // null - "A" was evicted due to expiry. cache.get("B") // null - "B" was evicted due to expiry. diff --git a/src/test/kotlin/com/igorwojda/cache/advancedlru/Solution.kt b/src/test/kotlin/com/igorwojda/cache/advancedlru/Solution.kt index b636e82a..b3850c60 100644 --- a/src/test/kotlin/com/igorwojda/cache/advancedlru/Solution.kt +++ b/src/test/kotlin/com/igorwojda/cache/advancedlru/Solution.kt @@ -1,83 +1,109 @@ package com.igorwojda.cache.advancedlru +import java.time.Clock +import java.time.Duration import java.util.* -// Implementation is using combination of HashMap and LinkedList. -// Time Complexity: O(1) -private object Solution1 { - class AdvancedLRUCache(private val capacity: Int) { - private val map: MutableMap = mutableMapOf() - private val priorityQueue: PriorityQueue = PriorityQueue() +// Implementation is using combination of HashMap and PriorityQueue. +// Time Complexity: O(N) (JVM priority queue is O(log(n)) on offer/poll methods and O(N) on remove(item) method) +internal object Solution1 { + class AdvancedLRUCache(private val capacity: Int, private val clock: Clock = Clock.systemDefaultZone()) : LRUCache { + private val map: MutableMap> = mutableMapOf() - fun put(key: String, value: Int, priority: Int, expiryTime: Long) { - if (map.containsKey(key)) { - remove(key) - } + private val expiryQueue: PriorityQueue> = PriorityQueue { item1, item2 -> + compareBy>({ it.expiryTime }).compare(item1, item2) + } - if (map.size == capacity) { - clearCache() - } + private val priorityQueue: PriorityQueue> = PriorityQueue { item1, item2 -> + compareBy>({ it.priority }, { it.lastUsed }).compare(item1, item2) + } - val item = CacheItem(key, value, priority, expiryTime) + override fun put(key: K, value: V, priority: Int, ttl: Duration) { + remove(key) + checkAndExpireCachedItems() + + val item = CacheItem(key, value, priority, clock.millis() + ttl.toMillis(), clock.millis()) map[key] = item - priorityQueue.add(item) + + expiryQueue.offer(item) + priorityQueue.offer(item) } - fun get(key: String): Int? { + override fun get(key: K): V? { val item = map[key] - return if (item == null || item.expiryTime < getSystemTimeForExpiry()) { + return if (item == null) { + null + } else if (item.expiryTime < clock.millis()) { + expiryQueue.remove(item) + priorityQueue.remove(item) null } else { - item.lastUsed = System.currentTimeMillis() + priorityQueue.remove(item) + priorityQueue.add(item.touch(clock.millis())) item.value } } - private fun remove(key: String) { + private fun remove(key: K) { val item = map[key] item?.let { - it.expiryTime = 0L // Mark as expired for next eviction + expiryQueue.remove(item) + priorityQueue.remove(item) map.remove(key) } } - private fun clearCache() { - while (priorityQueue.isNotEmpty() && priorityQueue.peek().expiryTime < getSystemTimeForExpiry()) { - val item = priorityQueue.poll() + private fun checkAndExpireCachedItems() { + if (map.size < capacity) return - if (map.containsKey(item.key) && map[item.key] == item) { - map.remove(item.key) - } + while (expiryQueue.isNotEmpty() && expiryQueue.peek().expiryTime < clock.millis()) { + val item = expiryQueue.poll() + map.remove(item.key) + priorityQueue.remove(item) } + if (map.size < capacity) return + if (priorityQueue.isEmpty()) return val item = priorityQueue.poll() + map.remove(item.key) + expiryQueue.remove(item) + } - if (map.containsKey(item.key) && map[item.key] == item) { - map.remove(item.key) + private class CacheItem( + val key: K, + val value: V, + val priority: Int, + val expiryTime: Long, + val lastUsed: Long + ) { + // only compare equality by `key` + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CacheItem<*, *>) return false + if (key == other.key) return true + return false } - } - private data class CacheItem( - val key: String, - var value: Int, - var priority: Int, - var expiryTime: Long, - ) : Comparable { - var lastUsed: Long = System.currentTimeMillis() - - override fun compareTo(other: CacheItem) = when { - expiryTime != other.expiryTime -> expiryTime.compareTo(other.expiryTime) - priority != other.priority -> priority.compareTo(other.priority) - else -> lastUsed.compareTo(other.lastUsed) + override fun hashCode(): Int { + return key.hashCode() + } + + fun touch( + lastUsed: Long = this.lastUsed + ) = CacheItem(key, value, priority, expiryTime, lastUsed) + + override fun toString(): String { + return "CacheItem(key='$key', value=$value, priority=$priority, expiryTime=$expiryTime, lastUsed=$lastUsed)" } } - // Returns fixed system time in milliseconds - private fun getSystemTimeForExpiry() = 1000 + override fun toString(): String { + return "AdvancedLRUCache(capacity=$capacity, clock=$clock, map=$map, priorityQueue=$expiryQueue)" + } } } diff --git a/src/test/kotlin/com/igorwojda/cache/advancedlru/Tests.kt b/src/test/kotlin/com/igorwojda/cache/advancedlru/Tests.kt new file mode 100644 index 00000000..bb0d5737 --- /dev/null +++ b/src/test/kotlin/com/igorwojda/cache/advancedlru/Tests.kt @@ -0,0 +1,120 @@ +package com.igorwojda.cache.advancedlru + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import java.time.Clock +import java.time.Duration +import java.time.Instant +import java.time.ZoneId + +class Tests { + // Easily switch between a known solution and Challenge code + private val classUnderTest: (capacity: Int, clock: Clock)->LRUCache = ::AdvancedLRUCache // or SolutionN::AdvancedLRUCache + + private val testClock = object : Clock() { + private var testTime = Instant.now() + override fun instant(): Instant { + return testTime + } + + fun incTime(duration: Duration) { + testTime += duration + } + + override fun withZone(zone: ZoneId?): Clock = TODO("Not yet implemented") + override fun getZone(): ZoneId = systemDefaultZone().zone + } + + @Test + fun `add and get immediately`() { + val cache = classUnderTest(2, testClock) + + cache.put("A", "apple", 0, Duration.ofMinutes(15)) + cache.get("A") shouldBeEqualTo "apple" + + cache.put("B", "bee", 0, Duration.ofMinutes(15)) + cache.get("B") shouldBeEqualTo "bee" + + cache.put("C", "cat", 0, Duration.ofMinutes(15)) + cache.get("C") shouldBeEqualTo "cat" + + cache.put("E", "echo", 0, Duration.ofMinutes(15)) + cache.get("E") shouldBeEqualTo "echo" + } + + @Test + fun `evict by priority`() { + val cache = classUnderTest(4, testClock) + + // all have the same expiry + cache.put("B", "bee", 3, Duration.ofMinutes(15)) + cache.put("A", "apple", 1, Duration.ofMinutes(15)) // lowest priority + cache.put("C", "cat", 5, Duration.ofMinutes(15)) + cache.put("D", "door", 7, Duration.ofMinutes(15)) + cache.put("E", "echo", 9, Duration.ofMinutes(15)) // causes eviction + + // This should be null because "A" was evicted due to lower priority and no items have reached expiry time + cache.get("A") shouldBeEqualTo null + cache.get("B") shouldBeEqualTo "bee" + cache.get("C") shouldBeEqualTo "cat" + cache.get("D") shouldBeEqualTo "door" + cache.get("E") shouldBeEqualTo "echo" + } + + @Test + fun `evict by priority and last used`() { + val cache = classUnderTest(4, testClock) + + // some have the same priority + cache.put("C", "cat", 1, Duration.ofMinutes(12)) // priority 1 + cache.put("A", "apple", 1, Duration.ofMinutes(20)) // priority 1 + cache.put("B", "bee", 1, Duration.ofMinutes(10)) // priority 1 + cache.put("D", "door", 7, Duration.ofMinutes(5)) + + // but are accessed most recently in a different order... + testClock.incTime(Duration.ofSeconds(1)) + cache.get("A") + testClock.incTime(Duration.ofSeconds(1)) + cache.get("C") + testClock.incTime(Duration.ofSeconds(1)) + cache.get("B") + testClock.incTime(Duration.ofSeconds(1)) + + cache.put("E", "echo", 9, Duration.ofMinutes(15)) // causes eviction + + // This should be null because "A" was evicted due to lower priority. + println(cache) + cache.get("A") shouldBeEqualTo null + cache.get("B") shouldBeEqualTo "bee" + cache.get("C") shouldBeEqualTo "cat" + cache.get("D") shouldBeEqualTo "door" + cache.get("E") shouldBeEqualTo "echo" + } + + @Test + fun `evict by expiry time`() { + val cache = classUnderTest(100, testClock) + + cache.put("A", "apple", 1, Duration.ofMinutes(15)) + cache.put("B", "bee", 3, Duration.ofMinutes(20)) + + testClock.incTime(Duration.ofMinutes(16)) + + // This should be null because "A" was evicted due to expiry. + cache.get("A") shouldBeEqualTo null + + testClock.incTime(Duration.ofMinutes(20)) + + cache.put("C", "cat", 5, Duration.ofMinutes(15)) // causes eviction + cache.put("D", "door", 5, Duration.ofMinutes(15)) // causes eviction + + // this should be null because "B" was evicted due to expiry and later inserts + cache.get("B") shouldBeEqualTo null + + testClock.incTime(Duration.ofMinutes(14)) + + // still here as clock has not moved past expiry + cache.get("C") shouldBeEqualTo "cat" + cache.get("D") shouldBeEqualTo "door" + } +} diff --git a/src/test/kotlin/com/igorwojda/cache/lru/Challenge.kt b/src/test/kotlin/com/igorwojda/cache/lru/Challenge.kt index b57ed3c9..652515d9 100644 --- a/src/test/kotlin/com/igorwojda/cache/lru/Challenge.kt +++ b/src/test/kotlin/com/igorwojda/cache/lru/Challenge.kt @@ -1,52 +1,20 @@ package com.igorwojda.cache.lru -import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Test +interface LRUCache { + val size: Int -class LRUCache(private val capacity: Int) { - val size: Int get() = TODO("Add your solution here") - - fun get(key: Int): String? { - TODO("Add your solution here") - } - - fun put(key: Int, value: String) { - TODO("Add your solution here") - } + fun get(key: K): V? + fun put(key: K, value: V) } -private class Test { - @Test - fun `lru cache is empty after creation`() { - val cache = LRUCache(3) +class LRUCacheImpl(private val capacity: Int) : LRUCache { + override val size: Int get() = TODO("Add your solution here") - cache.size shouldBeEqualTo 0 - } - - @Test - fun `oldest value is not removed from cache after capacity is exceeded`() { - val cache = LRUCache(2) - - cache.put(1, "Person1") - cache.put(2, "Person2") - cache.put(3, "Person3") - - cache.size shouldBeEqualTo 2 - cache.get(1) shouldBeEqualTo null - cache.get(2) shouldBeEqualTo "Person2" - cache.get(3) shouldBeEqualTo "Person3" + override fun get(key: K): V? { + TODO("Add your solution here") } - @Test - fun `retrieved element becomes most recently used`() { - val cache = LRUCache(2) - cache.put(1, "Person1") - cache.put(2, "Person2") - cache.get(1) - cache.put(3, "Person3") - - cache.get(1) shouldBeEqualTo "Person1" - cache.get(2) shouldBeEqualTo null - cache.get(3) shouldBeEqualTo "Person3" + override fun put(key: K, value: V) { + TODO("Add your solution here") } } diff --git a/src/test/kotlin/com/igorwojda/cache/lru/README.md b/src/test/kotlin/com/igorwojda/cache/lru/README.md index 745970dc..0f4cb1e5 100644 --- a/src/test/kotlin/com/igorwojda/cache/lru/README.md +++ b/src/test/kotlin/com/igorwojda/cache/lru/README.md @@ -8,7 +8,7 @@ removing the item that was least recently accessed or used. The overall run time complexity of each method should be `O(1)`. -[Challenge](Challenge.kt) | [Solution](Solution.kt) +[Challenge](Challenge.kt) | [Solution](Solution.kt) | [Tests](Tests.kt) ## Examples diff --git a/src/test/kotlin/com/igorwojda/cache/lru/Solution.kt b/src/test/kotlin/com/igorwojda/cache/lru/Solution.kt index 07d24eaf..e2197dc3 100644 --- a/src/test/kotlin/com/igorwojda/cache/lru/Solution.kt +++ b/src/test/kotlin/com/igorwojda/cache/lru/Solution.kt @@ -2,16 +2,16 @@ package com.igorwojda.cache.lru // Implementation is using combination of HashMap and LinkedList. // Time Complexity: O(1) -private object Solution1 { - class LRUCache(private val capacity: Int) { - private val map = mutableMapOf() +object Solution1 { + class LRUCacheImpl(private val capacity: Int) : LRUCache { + private val map = mutableMapOf>() - private var head: CacheItem? = null - private var tail: CacheItem? = null + private var head: CacheItem? = null + private var tail: CacheItem? = null - val size get() = map.size + override val size get() = map.size - fun put(key: Int, value: String) { + override fun put(key: K, value: V) { // Check if node exits val existingCacheItem = map[key] @@ -19,6 +19,9 @@ private object Solution1 { // Check Map capacity if (map.size >= capacity) { val removedNode = removeHead() + if (removedNode != null) { + map.remove(removedNode.key) + } map.remove(removedNode?.key) } @@ -33,7 +36,7 @@ private object Solution1 { } } - private fun addTail(cacheItem: CacheItem) { + private fun addTail(cacheItem: CacheItem) { // If list is empty if (head == null) { head = cacheItem @@ -45,7 +48,7 @@ private object Solution1 { tail = cacheItem } - private fun removeHead(): CacheItem? { + private fun removeHead(): CacheItem? { // Head exists if (head != null) { // Store current head to return @@ -64,7 +67,7 @@ private object Solution1 { return null } - fun get(key: Int): String? { + override fun get(key: K): V? { // Get the node val node = map[key] @@ -77,7 +80,7 @@ private object Solution1 { return node?.value } - private fun moveToTail(cacheItem: CacheItem) { + private fun moveToTail(cacheItem: CacheItem) { // Check if node is tail if (cacheItem != tail) { // Remove node from list @@ -93,33 +96,33 @@ private object Solution1 { } } - private data class CacheItem( - val key: Int, - var value: String, - var prev: CacheItem? = null, - var next: CacheItem? = null, + private data class CacheItem( + val key: K, + var value: V, + var prev: CacheItem? = null, + var next: CacheItem? = null, ) } } // Implementation using LinkedHashMap // Time Complexity: O(1) -private object Solution2 { - class LRUCache(private val capacity: Int) { - val size get() = linkedHashMap.size +object Solution2 { + class LRUCacheImpl(private val capacity: Int): LRUCache { + override val size get() = linkedHashMap.size private val linkedHashMap = object : - LinkedHashMap(capacity, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { + LinkedHashMap(capacity, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { return size > capacity } } - fun put(key: Int, value: String) { + override fun put(key: K, value: V) { linkedHashMap[key] = value } - fun get(key: Int): String? { + override fun get(key: K): V? { return linkedHashMap[key]?.also { linkedHashMap.remove(key) linkedHashMap[key] = it @@ -130,13 +133,13 @@ private object Solution2 { // Implementation using List // Time Complexity: O(n) -private object Solution3 { - class LRUCache(private val capacity: Int) { - private val list = mutableListOf>() +object Solution3 { + class LRUCacheImpl(private val capacity: Int): LRUCache { + private val list = mutableListOf>() - val size get() = list.size + override val size get() = list.size - fun get(key: Int): String? { + override fun get(key: K): V? { val value = list.firstOrNull { it.first == key }?.second if (value != null) { @@ -148,7 +151,7 @@ private object Solution3 { return list.firstOrNull { it.first == key }?.second } - fun put(key: Int, value: String) { + override fun put(key: K, value: V) { list.removeIf { it.first == key } list.add(Pair(key, value)) diff --git a/src/test/kotlin/com/igorwojda/cache/lru/Tests.kt b/src/test/kotlin/com/igorwojda/cache/lru/Tests.kt new file mode 100644 index 00000000..1b4767a5 --- /dev/null +++ b/src/test/kotlin/com/igorwojda/cache/lru/Tests.kt @@ -0,0 +1,43 @@ +package com.igorwojda.cache.lru + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test + +class Tests { + // Easily switch between a known solution and Challenge code + val classUnderTest: (Int)->LRUCache = ::LRUCacheImpl // or SolutionN::LRUCacheImpl + + @Test + fun `lru cache is empty after creation`() { + val cache = classUnderTest(3) + + cache.size shouldBeEqualTo 0 + } + + @Test + fun `oldest value is not removed from cache after capacity is exceeded`() { + val cache = classUnderTest(2) + + cache.put(1, "Value-1") + cache.put(2, "Value-2") + cache.put(3, "Value-3") + + cache.size shouldBeEqualTo 2 + cache.get(1) shouldBeEqualTo null + cache.get(2) shouldBeEqualTo "Value-2" + cache.get(3) shouldBeEqualTo "Value-3" + } + + @Test + fun `retrieved element becomes most recently used`() { + val cache = classUnderTest(2) + cache.put(1, "Value-1") + cache.put(2, "Value-2") + cache.get(1) + cache.put(3, "Value-3") + + cache.get(1) shouldBeEqualTo "Value-1" + cache.get(2) shouldBeEqualTo null + cache.get(3) shouldBeEqualTo "Value-3" + } +} diff --git a/src/test/kotlin/com/igorwojda/common/anycallback/Challenge.kt b/src/test/kotlin/com/igorwojda/common/anycallback/Challenge.kt index 918c2464..b007df2a 100644 --- a/src/test/kotlin/com/igorwojda/common/anycallback/Challenge.kt +++ b/src/test/kotlin/com/igorwojda/common/anycallback/Challenge.kt @@ -1,22 +1,7 @@ package com.igorwojda.common.anycallback -import org.amshove.kluent.shouldBeEqualTo -import org.junit.jupiter.api.Test - -private fun anyCallback(list: List, callback: (Int) -> Boolean): Boolean { +fun anyCallback(list: List, predicate: (T) -> Boolean): Boolean { TODO("Add your solution here") } -private class Test { - @Test - fun `any callback returns true`() { - val callback: ((Int) -> Boolean) = { it > 3 } - anyCallback(listOf(1, 2, 3, 4), callback) shouldBeEqualTo true - } - @Test - fun `any callback returns false`() { - val callback: ((Int) -> Boolean) = { it > 3 } - anyCallback(listOf(1, 2, 3), callback) shouldBeEqualTo false - } -} diff --git a/src/test/kotlin/com/igorwojda/common/anycallback/README.md b/src/test/kotlin/com/igorwojda/common/anycallback/README.md index 4fe4ed85..2aec6eea 100644 --- a/src/test/kotlin/com/igorwojda/common/anycallback/README.md +++ b/src/test/kotlin/com/igorwojda/common/anycallback/README.md @@ -10,7 +10,7 @@ Given list of integers and callback implement a **recursive** function which returns `true` if simple value in the list passed to callback returns `true`, otherwise return `false`. -[Challenge](Challenge.kt) | [Solution](Solution.kt) +[Challenge](Challenge.kt) | [Solution](Solution.kt) | [Tests](Tests.kt) ## Examples diff --git a/src/test/kotlin/com/igorwojda/common/anycallback/Solution.kt b/src/test/kotlin/com/igorwojda/common/anycallback/Solution.kt index 0cf55da8..398d114e 100644 --- a/src/test/kotlin/com/igorwojda/common/anycallback/Solution.kt +++ b/src/test/kotlin/com/igorwojda/common/anycallback/Solution.kt @@ -1,12 +1,41 @@ package com.igorwojda.common.anycallback -private object Solution1 { - private fun anyCallback(list: List, callback: (Int) -> Boolean): Boolean { +internal object Solution1 { + fun anyCallback(list: List, predicate: (T) -> Boolean): Boolean { if (list.size == 1) { - return callback(list.first()) + return predicate(list.first()) } - return callback(list.first()) || anyCallback(list.drop(1), callback) + return predicate(list.first()) || anyCallback(list.drop(1), predicate) + } +} + +internal object Solution2 { + fun anyCallback(list: List, predicate: (T) -> Boolean): Boolean { + if (list.isEmpty()) return false + return predicate(list.first()) || anyCallback(list.subList(1, list.size), predicate) + } +} + +internal object Solution3 { + fun anyCallback(list: List, predicate: (T) -> Boolean): Boolean { + fun _randomAccessOptimized(list: List, predicate: (T) -> Boolean): Boolean { + if (list.isEmpty()) return false + return predicate(list.first()) || _randomAccessOptimized(list.subList(1, list.size), predicate) + } + + fun _sequentialOptimized(list: List, predicate: (T) -> Boolean): Boolean { + if (list.isEmpty()) return false + return predicate(list.first()) || _sequentialOptimized(list.drop(1), predicate) + } + + + return if (list is RandomAccess) { + _randomAccessOptimized(list, predicate) + } + else { + _sequentialOptimized(list, predicate) + } } } diff --git a/src/test/kotlin/com/igorwojda/common/anycallback/Tests.kt b/src/test/kotlin/com/igorwojda/common/anycallback/Tests.kt new file mode 100644 index 00000000..c3140c23 --- /dev/null +++ b/src/test/kotlin/com/igorwojda/common/anycallback/Tests.kt @@ -0,0 +1,27 @@ +package com.igorwojda.common.anycallback + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test + +class Tests { + // Easily switch between a known solution and Challenge code + val functionUnderTest: (list: List, callback: (Int) -> Boolean)->Boolean = ::anyCallback // or SolutionN::anyCallback + + @Test + fun `any callback returns true`() { + val callback: ((Int) -> Boolean) = { it > 3 } + functionUnderTest(listOf(1, 2, 3, 4), callback) shouldBeEqualTo true + } + + @Test + fun `any callback returns false`() { + val callback: ((Int) -> Boolean) = { it > 3 } + functionUnderTest(listOf(1, 2, 3), callback) shouldBeEqualTo false + } + + @Test + fun `empty list returns false`() { + val callback: ((Int) -> Boolean) = { it > 3 } + functionUnderTest(emptyList(), callback) shouldBeEqualTo false + } +} \ No newline at end of file