From 5a797f470bac3b795cc11feeb93a752902787613 Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 10 Jun 2023 17:38:58 -0600 Subject: [PATCH] Add LRUCache (#6) --- Sources/Cache/Cache/LRUCache.swift | 101 +++++++++++++++++++++++++++ Tests/CacheTests/LRUCacheTests.swift | 93 ++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 Sources/Cache/Cache/LRUCache.swift create mode 100644 Tests/CacheTests/LRUCacheTests.swift diff --git a/Sources/Cache/Cache/LRUCache.swift b/Sources/Cache/Cache/LRUCache.swift new file mode 100644 index 0000000..16cecb0 --- /dev/null +++ b/Sources/Cache/Cache/LRUCache.swift @@ -0,0 +1,101 @@ +import Foundation + +/** +The `LRUCache` class is a cache that uses the Least Recently Used (LRU) algorithm to evict items when the cache capacity is exceeded. The LRU cache is implemented as a key-value store where the access to items is tracked and the least recently used ones are evicted when the capacity is reached. + +Use `LRUCache` to create a cache that automatically evicts items from memory when the cache capacity is exceeded. The cache contents are automatically loaded from the initial values dictionary when initialized. + +Note: You must make sure that the specified key type conforms to the `Hashable` protocol. + +Error Handling: The set(value:forKey:) function does not throw any error. Instead, when the cache capacity is exceeded, the least recently used item is automatically evicted from the cache. + +The `LRUCache` class is a subclass of the `Cache` class. You can use its `capacity` property to specify the maximum number of key-value pairs that the cache can hold. +*/ +public class LRUCache: Cache { + private var keys: [Key] + + /// The maximum capacity of the cache. + public let capacity: UInt + + /** + Initializes a new `LRUCache` instance with the specified capacity. + + - Parameter capacity: The maximum number of key-value pairs that the cache can hold. + */ + public init(capacity: UInt) { + self.keys = [] + self.capacity = capacity + + super.init() + } + + /** + Initializes a new `LRUCache` instance with the specified initial values dictionary. + + The contents of the dictionary are loaded into the cache, and the capacity is set to the number of key-value pairs in the dictionary. + + - Parameter initialValues: A dictionary of key-value pairs to load into the cache initially. + */ + public required init(initialValues: [Key: Value] = [:]) { + let keys = Array(initialValues.keys) + + self.keys = keys + self.capacity = UInt(keys.count) + + super.init(initialValues: initialValues) + } + + public override func get(_ key: Key, as: Output.Type = Output.self) -> Output? { + guard let value = super.get(key, as: Output.self) else { + return nil + } + + updateKeys(recentlyUsed: key) + + return value + } + + public override func set(value: Value, forKey key: Key) { + super.set(value: value, forKey: key) + + updateKeys(recentlyUsed: key) + checkCapacity() + } + + public override func remove(_ key: Key) { + super.remove(key) + + if let index = keys.firstIndex(of: key) { + keys.remove(at: index) + } + } + + public override func contains(_ key: Key) -> Bool { + guard super.contains(key) else { + return false + } + + updateKeys(recentlyUsed: key) + + return true + } + + // MARK: - Private Helpers + + private func checkCapacity() { + guard + keys.count > capacity, + let keyToRemove = keys.first + else { return } + + remove(keyToRemove) + } + + private func updateKeys(recentlyUsed: Key) { + if let index = keys.firstIndex(of: recentlyUsed) { + keys.remove(at: index) + } + + keys.append(recentlyUsed) + } +} diff --git a/Tests/CacheTests/LRUCacheTests.swift b/Tests/CacheTests/LRUCacheTests.swift new file mode 100644 index 0000000..e371a01 --- /dev/null +++ b/Tests/CacheTests/LRUCacheTests.swift @@ -0,0 +1,93 @@ +import XCTest +@testable import Cache + +final class LRUCacheTests: XCTestCase { + func testLRUCacheCapacity() { + let cache = LRUCache(capacity: 3) + + // Add some key-value pairs + cache["one"] = 1 + cache["two"] = 2 + cache["three"] = 3 + + // Test that the cache has the expected contents + XCTAssertEqual(cache["one"], 1) + XCTAssertEqual(cache["two"], 2) + XCTAssertEqual(cache["three"], 3) + + // Add a new key-value pair to exceed the capacity + cache["four"] = 4 + + // Test that the least recently used key was removed + XCTAssertNil(cache["one"]) + + // Test that the cache has the expected contents + XCTAssertEqual(cache["two"], 2) + XCTAssertEqual(cache["three"], 3) + XCTAssertEqual(cache["four"], 4) + + // Access an existing key to promote it to the end of the keys array + XCTAssert(cache.contains("two")) + + // Add another key-value pair to exceed the capacity + cache["five"] = 5 + + // Test that the least recently used key was removed + XCTAssertNil(cache["three"]) + + // Test that the cache has the expected contents + XCTAssertEqual(cache["two"], 2) + XCTAssertEqual(cache["four"], 4) + XCTAssertEqual(cache["five"], 5) + + // Remove a key and test that it was removed from both the cache and the keys array + cache["two"] = nil + XCTAssertNil(cache["two"]) + XCTAssertFalse(cache.contains("two")) + } + + func testLRUCacheInitialValues() { + let cache = LRUCache( + initialValues: [ + "one": 1, + "two": 2, + "three": 3 + ] + ) + + // Test that the cache has the expected contents + XCTAssertEqual(cache["one"], 1) + XCTAssertEqual(cache["two"], 2) + XCTAssertEqual(cache["three"], 3) + + // Add a new key-value pair to exceed the capacity + cache["four"] = 4 + + // Test that the least recently used key was removed + XCTAssertNil(cache["one"]) + + // Test that the cache has the expected contents + XCTAssertEqual(cache["two"], 2) + XCTAssertEqual(cache["three"], 3) + XCTAssertEqual(cache["four"], 4) + + // Access an existing key to promote it to the end of the keys array + XCTAssert(cache.contains("two")) + + // Add another key-value pair to exceed the capacity + cache["five"] = 5 + + // Test that the least recently used key was removed + XCTAssertNil(cache["three"]) + + // Test that the cache has the expected contents + XCTAssertEqual(cache["two"], 2) + XCTAssertEqual(cache["four"], 4) + XCTAssertEqual(cache["five"], 5) + + // Remove a key and test that it was removed from both the cache and the keys array + cache["two"] = nil + XCTAssertNil(cache["two"]) + XCTAssertFalse(cache.contains("two")) + } +}