1use lru::LruCache;
2use std::num::NonZeroUsize;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Mutex;
5
6pub struct Cache {
12 inner: Mutex<LruCache<String, i32>>,
13 hits: AtomicU64,
14 misses: AtomicU64,
15}
16
17impl Cache {
18 pub fn new(capacity: usize) -> Self {
24 let safe_capacity = capacity.max(1);
26 Self {
27 inner: Mutex::new(LruCache::new(
28 NonZeroUsize::new(safe_capacity).expect("capacity should be non-zero after max(1)"),
29 )),
30 hits: AtomicU64::new(0),
31 misses: AtomicU64::new(0),
32 }
33 }
34
35 pub fn get(&self, key: &str) -> Option<i32> {
46 let mut cache = self.inner.lock().ok()?;
48 let result = cache.get(key).copied();
49
50 if result.is_some() {
51 self.hits.fetch_add(1, Ordering::Relaxed);
52 } else {
53 self.misses.fetch_add(1, Ordering::Relaxed);
54 }
55
56 result
57 }
58
59 pub fn put(&self, key: String, value: i32) {
68 if let Ok(mut cache) = self.inner.lock() {
70 cache.put(key, value);
71 }
72 }
73
74 pub fn hits(&self) -> u64 {
80 self.hits.load(Ordering::Relaxed)
81 }
82
83 pub fn misses(&self) -> u64 {
89 self.misses.load(Ordering::Relaxed)
90 }
91
92 pub fn hit_rate(&self) -> f64 {
99 let hits = self.hits();
100 let misses = self.misses();
101 let total = hits + misses;
102
103 if total == 0 {
104 0.0
105 } else {
106 (hits as f64 / total as f64) * 100.0
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_cache_zero_capacity_does_not_panic() {
117 let cache = Cache::new(0);
120 cache.put("test".to_string(), 42);
121 assert_eq!(cache.get("test"), Some(42));
122 }
123
124 #[test]
125 fn test_cache_basic_operations() {
126 let cache = Cache::new(10);
128
129 assert_eq!(cache.get("key1"), None);
130
131 cache.put("key1".to_string(), 100);
132 assert_eq!(cache.get("key1"), Some(100));
133
134 cache.put("key2".to_string(), 200);
135 assert_eq!(cache.get("key2"), Some(200));
136 assert_eq!(cache.get("key1"), Some(100));
137 }
138
139 #[test]
140 fn test_cache_lru_eviction() {
141 let cache = Cache::new(2);
143
144 cache.put("key1".to_string(), 1);
145 cache.put("key2".to_string(), 2);
146 cache.put("key3".to_string(), 3); assert_eq!(cache.get("key1"), None); assert_eq!(cache.get("key2"), Some(2));
150 assert_eq!(cache.get("key3"), Some(3));
151 }
152
153 #[test]
154 fn test_cache_hit_miss_tracking() {
155 let cache = Cache::new(10);
157
158 assert_eq!(cache.hits(), 0);
160 assert_eq!(cache.misses(), 0);
161 assert_eq!(cache.hit_rate(), 0.0);
162
163 assert_eq!(cache.get("key1"), None);
165 assert_eq!(cache.hits(), 0);
166 assert_eq!(cache.misses(), 1);
167 assert_eq!(cache.hit_rate(), 0.0);
168
169 cache.put("key1".to_string(), 100);
171
172 assert_eq!(cache.get("key1"), Some(100));
174 assert_eq!(cache.hits(), 1);
175 assert_eq!(cache.misses(), 1);
176 assert_eq!(cache.hit_rate(), 50.0);
177
178 assert_eq!(cache.get("key1"), Some(100));
180 assert_eq!(cache.hits(), 2);
181 assert_eq!(cache.misses(), 1);
182 assert!((cache.hit_rate() - 66.666).abs() < 0.01);
183
184 assert_eq!(cache.get("key2"), None);
186 assert_eq!(cache.hits(), 2);
187 assert_eq!(cache.misses(), 2);
188 assert_eq!(cache.hit_rate(), 50.0);
189 }
190}