use std::collections::HashMap;
use std::hash::Hash;
struct CacheEntry<V> {
value: V,
visited: bool,
}
pub struct SieveCache<K, V> {
capacity: usize,
index: HashMap<K, usize>,
entries: Vec<Option<(K, CacheEntry<V>)>>,
hand: usize,
size: usize,
hits: u64,
misses: u64,
}
impl<K: Hash + Eq + Clone, V> SieveCache<K, V> {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "SieveCache capacity must be > 0");
let mut entries = Vec::with_capacity(capacity);
entries.resize_with(capacity, || None);
Self {
capacity,
index: HashMap::with_capacity(capacity),
entries,
hand: 0,
size: 0,
hits: 0,
misses: 0,
}
}
pub fn get(&mut self, key: &K) -> Option<&V> {
if let Some(&slot) = self.index.get(key) {
if let Some((_, ref mut entry)) = self.entries[slot] {
entry.visited = true;
self.hits += 1;
return Some(&entry.value);
}
}
self.misses += 1;
None
}
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
if let Some(&slot) = self.index.get(key) {
if let Some((_, ref mut entry)) = self.entries[slot] {
entry.visited = true;
self.hits += 1;
return Some(&mut entry.value);
}
}
self.misses += 1;
None
}
pub fn insert(&mut self, key: K, value: V) {
if let Some(&slot) = self.index.get(&key) {
if let Some((_, ref mut entry)) = self.entries[slot] {
entry.value = value;
entry.visited = true;
return;
}
}
if self.size >= self.capacity {
self.evict();
}
let free_slot = self.find_free_slot();
self.entries[free_slot] = Some((
key.clone(),
CacheEntry {
value,
visited: false,
},
));
self.index.insert(key, free_slot);
self.size += 1;
}
pub fn remove(&mut self, key: &K) -> Option<V> {
if let Some(slot) = self.index.remove(key) {
if let Some((_, entry)) = self.entries[slot].take() {
self.size -= 1;
return Some(entry.value);
}
}
None
}
pub fn contains_key(&self, key: &K) -> bool {
self.index.contains_key(key)
}
pub fn len(&self) -> usize {
self.size
}
pub fn is_empty(&self) -> bool {
self.size == 0
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn hits(&self) -> u64 {
self.hits
}
pub fn misses(&self) -> u64 {
self.misses
}
pub fn hit_ratio(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
return 0.0;
}
self.hits as f64 / total as f64
}
pub fn clear(&mut self) {
self.index.clear();
for slot in &mut self.entries {
*slot = None;
}
self.hand = 0;
self.size = 0;
self.hits = 0;
self.misses = 0;
}
fn evict(&mut self) {
loop {
if let Some((ref key, ref mut entry)) = self.entries[self.hand] {
if entry.visited {
entry.visited = false;
self.advance_hand();
} else {
let evicted_key = key.clone();
self.entries[self.hand] = None;
self.index.remove(&evicted_key);
self.size -= 1;
return;
}
} else {
self.advance_hand();
}
}
}
fn advance_hand(&mut self) {
self.hand = (self.hand + 1) % self.capacity;
}
fn find_free_slot(&self) -> usize {
let mut idx = self.hand;
for _ in 0..self.capacity {
if self.entries[idx].is_none() {
return idx;
}
idx = (idx + 1) % self.capacity;
}
unreachable!("find_free_slot called but no free slot exists");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_insert_and_get() {
let mut cache = SieveCache::new(4);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
assert_eq!(cache.get(&"c"), Some(&3));
assert_eq!(cache.get(&"d"), None);
assert_eq!(cache.len(), 3);
}
#[test]
fn test_update_existing_key() {
let mut cache = SieveCache::new(4);
cache.insert("a", 1);
cache.insert("a", 42);
assert_eq!(cache.get(&"a"), Some(&42));
assert_eq!(cache.len(), 1);
}
#[test]
fn test_eviction_behavior() {
let mut cache = SieveCache::new(3);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
assert_eq!(cache.len(), 3);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
cache.insert("d", 4);
assert_eq!(cache.len(), 3);
assert!(!cache.contains_key(&"c"));
assert!(cache.contains_key(&"a"));
assert!(cache.contains_key(&"b"));
assert!(cache.contains_key(&"d"));
}
#[test]
fn test_eviction_clears_visited_bits() {
let mut cache = SieveCache::new(3);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
cache.get(&"a");
cache.get(&"b");
cache.get(&"c");
cache.insert("d", 4);
assert_eq!(cache.len(), 3);
let remaining: Vec<bool> = ["a", "b", "c"]
.iter()
.map(|k| cache.contains_key(k))
.collect();
let evicted_count = remaining.iter().filter(|&&v| !v).count();
assert_eq!(
evicted_count, 1,
"Exactly one of the original entries should have been evicted"
);
assert!(cache.contains_key(&"d"));
}
#[test]
fn test_hit_ratio_tracking() {
let mut cache = SieveCache::new(4);
cache.insert("a", 1);
cache.get(&"a");
cache.get(&"x");
cache.get(&"y");
assert_eq!(cache.hits(), 1);
assert_eq!(cache.misses(), 2);
let ratio = cache.hit_ratio();
let expected = 1.0 / 3.0;
assert!(
(ratio - expected).abs() < 1e-9,
"Expected hit ratio ~{expected}, got {ratio}"
);
}
#[test]
fn test_capacity_and_clear() {
let mut cache = SieveCache::new(2);
assert_eq!(cache.capacity(), 2);
cache.insert(1, "one");
cache.insert(2, "two");
assert_eq!(cache.len(), 2);
assert!(!cache.is_empty());
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert_eq!(cache.hits(), 0);
assert_eq!(cache.misses(), 0);
assert_eq!(cache.get(&1), None);
}
#[test]
fn test_remove() {
let mut cache = SieveCache::new(4);
cache.insert("a", 10);
cache.insert("b", 20);
assert_eq!(cache.remove(&"a"), Some(10));
assert_eq!(cache.len(), 1);
assert!(!cache.contains_key(&"a"));
assert_eq!(cache.remove(&"z"), None);
}
#[test]
fn test_get_mut() {
let mut cache = SieveCache::new(4);
cache.insert("a", vec![1, 2, 3]);
if let Some(v) = cache.get_mut(&"a") {
v.push(4);
}
assert_eq!(cache.get(&"a"), Some(&vec![1, 2, 3, 4]));
}
#[test]
fn test_single_capacity() {
let mut cache = SieveCache::new(1);
cache.insert("a", 1);
assert_eq!(cache.get(&"a"), Some(&1));
cache.insert("b", 2);
assert_eq!(cache.len(), 1);
assert!(!cache.contains_key(&"a"));
assert_eq!(cache.get(&"b"), Some(&2));
}
#[test]
fn test_hit_ratio_no_accesses() {
let cache: SieveCache<String, i32> = SieveCache::new(4);
assert!((cache.hit_ratio() - 0.0).abs() < f64::EPSILON);
}
}