Skip to main content

modo/cache/
lru.rs

1use std::collections::{HashMap, VecDeque};
2use std::hash::Hash;
3use std::num::NonZeroUsize;
4
5/// A fixed-capacity, in-memory least-recently-used (LRU) cache.
6///
7/// When the cache is full, inserting a new entry evicts the entry that was
8/// least recently accessed. Updating an existing key moves it to the
9/// most-recently-used position without consuming extra capacity.
10///
11/// `LruCache` is not `Sync`; wrap it in [`std::sync::RwLock`] or
12/// [`std::sync::Mutex`] when sharing across threads.
13///
14/// # Type parameters
15///
16/// - `K` — key type; must implement [`Eq`], [`Hash`], and [`Clone`].
17/// - `V` — value type; no bounds required.
18///
19/// # Examples
20///
21/// ```
22/// use std::num::NonZeroUsize;
23/// use modo::cache::LruCache;
24///
25/// let mut cache: LruCache<&str, u32> = LruCache::new(NonZeroUsize::new(2).unwrap());
26/// cache.put("a", 1);
27/// cache.put("b", 2);
28/// assert_eq!(cache.get(&"a"), Some(&1));
29///
30/// // Inserting a third entry evicts "b" (least recently used).
31/// cache.put("c", 3);
32/// assert!(cache.get(&"b").is_none());
33/// ```
34pub struct LruCache<K, V> {
35    map: HashMap<K, V>,
36    order: VecDeque<K>,
37    capacity: NonZeroUsize,
38}
39
40impl<K: Eq + Hash + Clone, V> LruCache<K, V> {
41    /// Creates a new `LruCache` with the given maximum `capacity`.
42    ///
43    /// The cache will hold at most `capacity` entries at any time. When a new
44    /// entry is inserted into a full cache, the least-recently-used entry is
45    /// evicted first.
46    pub fn new(capacity: NonZeroUsize) -> Self {
47        Self {
48            map: HashMap::with_capacity(capacity.get()),
49            order: VecDeque::with_capacity(capacity.get()),
50            capacity,
51        }
52    }
53
54    /// Returns a reference to the value associated with `key`, or `None` if
55    /// the key is not present.
56    ///
57    /// Accessing a key moves it to the most-recently-used position, making it
58    /// the last candidate for eviction.
59    pub fn get(&mut self, key: &K) -> Option<&V> {
60        if self.map.contains_key(key) {
61            // Move to back (most recently used)
62            if let Some(pos) = self.order.iter().position(|k| k == key) {
63                self.order.remove(pos);
64            }
65            self.order.push_back(key.clone());
66            self.map.get(key)
67        } else {
68            None
69        }
70    }
71
72    /// Inserts or updates the entry for `key` with the given `value`.
73    ///
74    /// - If `key` already exists, its value is replaced and it moves to the
75    ///   most-recently-used position.
76    /// - If the cache is at capacity and `key` is new, the least-recently-used
77    ///   entry is evicted before the new entry is inserted.
78    pub fn put(&mut self, key: K, value: V) {
79        if self.map.contains_key(&key) {
80            // Update existing — remove from order, will re-add at back
81            if let Some(pos) = self.order.iter().position(|k| k == &key) {
82                self.order.remove(pos);
83            }
84        } else if self.map.len() >= self.capacity.get() {
85            // Evict least recently used (front of deque)
86            if let Some(evicted) = self.order.pop_front() {
87                self.map.remove(&evicted);
88            }
89        }
90        self.map.insert(key.clone(), value);
91        self.order.push_back(key);
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn cache(cap: usize) -> LruCache<String, String> {
100        LruCache::new(NonZeroUsize::new(cap).unwrap())
101    }
102
103    #[test]
104    fn get_returns_none_for_missing() {
105        let mut c = cache(2);
106        assert!(c.get(&"x".into()).is_none());
107    }
108
109    #[test]
110    fn put_and_get() {
111        let mut c = cache(2);
112        c.put("a".into(), "1".into());
113        assert_eq!(c.get(&"a".into()), Some(&"1".into()));
114    }
115
116    #[test]
117    fn evicts_lru_on_capacity() {
118        let mut c = cache(2);
119        c.put("a".into(), "1".into());
120        c.put("b".into(), "2".into());
121        c.put("c".into(), "3".into()); // evicts "a"
122        assert!(c.get(&"a".into()).is_none());
123        assert_eq!(c.get(&"b".into()), Some(&"2".into()));
124        assert_eq!(c.get(&"c".into()), Some(&"3".into()));
125    }
126
127    #[test]
128    fn get_refreshes_lru_order() {
129        let mut c = cache(2);
130        c.put("a".into(), "1".into());
131        c.put("b".into(), "2".into());
132        c.get(&"a".into()); // refresh "a"
133        c.put("c".into(), "3".into()); // evicts "b" (not "a")
134        assert_eq!(c.get(&"a".into()), Some(&"1".into()));
135        assert!(c.get(&"b".into()).is_none());
136    }
137
138    #[test]
139    fn put_updates_existing() {
140        let mut c = cache(2);
141        c.put("a".into(), "1".into());
142        c.put("a".into(), "2".into());
143        assert_eq!(c.get(&"a".into()), Some(&"2".into()));
144    }
145
146    #[test]
147    fn capacity_one() {
148        let mut c = cache(1);
149        c.put("a".into(), "1".into());
150        c.put("b".into(), "2".into());
151        assert!(c.get(&"a".into()).is_none());
152        assert_eq!(c.get(&"b".into()), Some(&"2".into()));
153    }
154}