1use std::collections::HashMap;
2use std::hash::Hash;
3use std::num::NonZeroUsize;
4
5struct CacheEntry<V> {
6 value: V,
7 last_used: u64,
8}
9
10pub struct BoundedLruCache<K, V> {
16 entries: HashMap<K, CacheEntry<V>>,
17 cap: NonZeroUsize,
18 clock: u64,
19}
20
21impl<K, V> BoundedLruCache<K, V>
22where
23 K: Clone + Eq + Hash,
24{
25 pub fn new(cap: NonZeroUsize) -> Self {
26 Self {
27 entries: HashMap::with_capacity(cap.get()),
28 cap,
29 clock: 0,
30 }
31 }
32
33 pub fn with_capacity_at_least_one(cap: usize) -> Self {
34 let cap = NonZeroUsize::new(cap).unwrap_or(NonZeroUsize::MIN);
35 Self::new(cap)
36 }
37
38 pub fn len(&self) -> usize {
39 self.entries.len()
40 }
41
42 pub fn is_empty(&self) -> bool {
43 self.entries.is_empty()
44 }
45
46 pub fn cap(&self) -> NonZeroUsize {
47 self.cap
48 }
49
50 pub fn contains(&self, key: &K) -> bool {
51 self.entries.contains_key(key)
52 }
53
54 pub fn get(&mut self, key: &K) -> Option<&V> {
55 let tick = self.next_tick();
56 let entry = self.entries.get_mut(key)?;
57 entry.last_used = tick;
58 Some(&entry.value)
59 }
60
61 pub fn peek(&self, key: &K) -> Option<&V> {
62 self.entries.get(key).map(|entry| &entry.value)
63 }
64
65 pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
66 let tick = self.next_tick();
67 let entry = self.entries.get_mut(key)?;
68 entry.last_used = tick;
69 Some(&mut entry.value)
70 }
71
72 pub fn push(&mut self, key: K, value: V) -> Option<(K, V)> {
73 let tick = self.next_tick();
74 if let Some(entry) = self.entries.get_mut(&key) {
75 entry.last_used = tick;
76 let old_value = std::mem::replace(&mut entry.value, value);
77 return Some((key, old_value));
78 }
79
80 let evicted = if self.entries.len() == self.cap.get() {
81 self.pop_lru()
82 } else {
83 None
84 };
85 self.entries.insert(
86 key,
87 CacheEntry {
88 value,
89 last_used: tick,
90 },
91 );
92 evicted
93 }
94
95 pub fn put(&mut self, key: K, value: V) -> Option<V> {
96 self.push(key, value).map(|(_, value)| value)
97 }
98
99 pub fn pop_lru(&mut self) -> Option<(K, V)> {
100 let key = self
101 .entries
102 .iter()
103 .min_by_key(|(_, entry)| entry.last_used)
104 .map(|(key, _)| key.clone())?;
105 self.entries
106 .remove_entry(&key)
107 .map(|(key, entry)| (key, entry.value))
108 }
109
110 pub fn pop(&mut self, key: &K) -> Option<V> {
111 self.entries.remove(key).map(|entry| entry.value)
112 }
113
114 pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
115 let mut entries = self.entries.iter().collect::<Vec<_>>();
116 entries.sort_by_key(|(_, entry)| std::cmp::Reverse(entry.last_used));
117 entries.into_iter().map(|(key, entry)| (key, &entry.value))
118 }
119
120 fn next_tick(&mut self) -> u64 {
121 self.clock = self.clock.saturating_add(1);
122 self.clock
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::BoundedLruCache;
129
130 fn cache<K, V>(cap: usize) -> BoundedLruCache<K, V>
131 where
132 K: Clone + Eq + std::hash::Hash,
133 {
134 BoundedLruCache::with_capacity_at_least_one(cap)
135 }
136
137 #[test]
138 fn clamped_constructor_uses_minimum_nonzero_capacity() {
139 let mut cache = BoundedLruCache::with_capacity_at_least_one(0);
140 assert_eq!(cache.cap().get(), 1);
141 assert_eq!(cache.push("a", 1), None);
142 assert_eq!(cache.push("b", 2), Some(("a", 1)));
143 assert_eq!(cache.get(&"b"), Some(&2));
144 }
145
146 #[test]
147 fn get_promotes_entry_and_push_evicts_lru() {
148 let mut cache = cache(2);
149 assert_eq!(cache.push("a", 1), None);
150 assert_eq!(cache.push("b", 2), None);
151
152 assert_eq!(cache.get(&"a"), Some(&1));
153 assert_eq!(cache.push("c", 3), Some(("b", 2)));
154
155 assert!(cache.contains(&"a"));
156 assert!(cache.contains(&"c"));
157 assert!(!cache.contains(&"b"));
158 }
159
160 #[test]
161 fn push_existing_replaces_value_and_keeps_capacity() {
162 let mut cache = cache(2);
163 cache.push("a", 1);
164 cache.push("b", 2);
165
166 assert_eq!(cache.push("a", 3), Some(("a", 1)));
167 assert_eq!(cache.len(), 2);
168 assert_eq!(cache.get(&"a"), Some(&3));
169 }
170
171 #[test]
172 fn pop_removes_requested_entry_and_preserves_lru_order() {
173 let mut cache = cache(3);
174 cache.push("a", 1);
175 cache.push("b", 2);
176 cache.push("c", 3);
177
178 assert_eq!(cache.pop(&"b"), Some(2));
179 assert_eq!(cache.get(&"a"), Some(&1));
180 assert_eq!(cache.pop_lru(), Some(("c", 3)));
181 assert_eq!(cache.len(), 1);
182 }
183
184 #[test]
185 fn peek_reads_without_promoting_entry() {
186 let mut cache = cache(2);
187 cache.push("a", 1);
188 cache.push("b", 2);
189
190 assert_eq!(cache.peek(&"a"), Some(&1));
191 assert_eq!(cache.push("c", 3), Some(("a", 1)));
192 }
193
194 #[test]
195 fn iter_reports_mru_to_lru_entries() {
196 let mut cache = cache(3);
197 cache.push("a", 1);
198 cache.push("b", 2);
199 cache.get(&"a");
200
201 let entries: Vec<_> = cache.iter().map(|(key, value)| (*key, *value)).collect();
202 assert_eq!(entries, vec![("a", 1), ("b", 2)]);
203 }
204}