Skip to content

Commit 5a797f4

Browse files
authored
Add LRUCache (#6)
1 parent e8bb8f8 commit 5a797f4

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

Sources/Cache/Cache/LRUCache.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import Foundation
2+
3+
/**
4+
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.
5+
6+
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.
7+
8+
Note: You must make sure that the specified key type conforms to the `Hashable` protocol.
9+
10+
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.
11+
12+
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.
13+
*/
14+
public class LRUCache<Key: Hashable, Value>: Cache<Key, Value> {
15+
private var keys: [Key]
16+
17+
/// The maximum capacity of the cache.
18+
public let capacity: UInt
19+
20+
/**
21+
Initializes a new `LRUCache` instance with the specified capacity.
22+
23+
- Parameter capacity: The maximum number of key-value pairs that the cache can hold.
24+
*/
25+
public init(capacity: UInt) {
26+
self.keys = []
27+
self.capacity = capacity
28+
29+
super.init()
30+
}
31+
32+
/**
33+
Initializes a new `LRUCache` instance with the specified initial values dictionary.
34+
35+
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.
36+
37+
- Parameter initialValues: A dictionary of key-value pairs to load into the cache initially.
38+
*/
39+
public required init(initialValues: [Key: Value] = [:]) {
40+
let keys = Array(initialValues.keys)
41+
42+
self.keys = keys
43+
self.capacity = UInt(keys.count)
44+
45+
super.init(initialValues: initialValues)
46+
}
47+
48+
public override func get<Output>(_ key: Key, as: Output.Type = Output.self) -> Output? {
49+
guard let value = super.get(key, as: Output.self) else {
50+
return nil
51+
}
52+
53+
updateKeys(recentlyUsed: key)
54+
55+
return value
56+
}
57+
58+
public override func set(value: Value, forKey key: Key) {
59+
super.set(value: value, forKey: key)
60+
61+
updateKeys(recentlyUsed: key)
62+
checkCapacity()
63+
}
64+
65+
public override func remove(_ key: Key) {
66+
super.remove(key)
67+
68+
if let index = keys.firstIndex(of: key) {
69+
keys.remove(at: index)
70+
}
71+
}
72+
73+
public override func contains(_ key: Key) -> Bool {
74+
guard super.contains(key) else {
75+
return false
76+
}
77+
78+
updateKeys(recentlyUsed: key)
79+
80+
return true
81+
}
82+
83+
// MARK: - Private Helpers
84+
85+
private func checkCapacity() {
86+
guard
87+
keys.count > capacity,
88+
let keyToRemove = keys.first
89+
else { return }
90+
91+
remove(keyToRemove)
92+
}
93+
94+
private func updateKeys(recentlyUsed: Key) {
95+
if let index = keys.firstIndex(of: recentlyUsed) {
96+
keys.remove(at: index)
97+
}
98+
99+
keys.append(recentlyUsed)
100+
}
101+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import XCTest
2+
@testable import Cache
3+
4+
final class LRUCacheTests: XCTestCase {
5+
func testLRUCacheCapacity() {
6+
let cache = LRUCache<String, Int>(capacity: 3)
7+
8+
// Add some key-value pairs
9+
cache["one"] = 1
10+
cache["two"] = 2
11+
cache["three"] = 3
12+
13+
// Test that the cache has the expected contents
14+
XCTAssertEqual(cache["one"], 1)
15+
XCTAssertEqual(cache["two"], 2)
16+
XCTAssertEqual(cache["three"], 3)
17+
18+
// Add a new key-value pair to exceed the capacity
19+
cache["four"] = 4
20+
21+
// Test that the least recently used key was removed
22+
XCTAssertNil(cache["one"])
23+
24+
// Test that the cache has the expected contents
25+
XCTAssertEqual(cache["two"], 2)
26+
XCTAssertEqual(cache["three"], 3)
27+
XCTAssertEqual(cache["four"], 4)
28+
29+
// Access an existing key to promote it to the end of the keys array
30+
XCTAssert(cache.contains("two"))
31+
32+
// Add another key-value pair to exceed the capacity
33+
cache["five"] = 5
34+
35+
// Test that the least recently used key was removed
36+
XCTAssertNil(cache["three"])
37+
38+
// Test that the cache has the expected contents
39+
XCTAssertEqual(cache["two"], 2)
40+
XCTAssertEqual(cache["four"], 4)
41+
XCTAssertEqual(cache["five"], 5)
42+
43+
// Remove a key and test that it was removed from both the cache and the keys array
44+
cache["two"] = nil
45+
XCTAssertNil(cache["two"])
46+
XCTAssertFalse(cache.contains("two"))
47+
}
48+
49+
func testLRUCacheInitialValues() {
50+
let cache = LRUCache<String, Int>(
51+
initialValues: [
52+
"one": 1,
53+
"two": 2,
54+
"three": 3
55+
]
56+
)
57+
58+
// Test that the cache has the expected contents
59+
XCTAssertEqual(cache["one"], 1)
60+
XCTAssertEqual(cache["two"], 2)
61+
XCTAssertEqual(cache["three"], 3)
62+
63+
// Add a new key-value pair to exceed the capacity
64+
cache["four"] = 4
65+
66+
// Test that the least recently used key was removed
67+
XCTAssertNil(cache["one"])
68+
69+
// Test that the cache has the expected contents
70+
XCTAssertEqual(cache["two"], 2)
71+
XCTAssertEqual(cache["three"], 3)
72+
XCTAssertEqual(cache["four"], 4)
73+
74+
// Access an existing key to promote it to the end of the keys array
75+
XCTAssert(cache.contains("two"))
76+
77+
// Add another key-value pair to exceed the capacity
78+
cache["five"] = 5
79+
80+
// Test that the least recently used key was removed
81+
XCTAssertNil(cache["three"])
82+
83+
// Test that the cache has the expected contents
84+
XCTAssertEqual(cache["two"], 2)
85+
XCTAssertEqual(cache["four"], 4)
86+
XCTAssertEqual(cache["five"], 5)
87+
88+
// Remove a key and test that it was removed from both the cache and the keys array
89+
cache["two"] = nil
90+
XCTAssertNil(cache["two"])
91+
XCTAssertFalse(cache.contains("two"))
92+
}
93+
}

0 commit comments

Comments
 (0)