proximipy/caching/bounded/
bounded_linear_cache.rs

1use std::collections::HashMap;
2use std::hash::Hash;
3
4use crate::numerics::comp::ApproxComparable;
5
6use crate::caching::approximate_cache::ApproximateCache;
7
8use super::linked_list::DoublyLinkedList;
9use super::list_node::{Node, SharedNode};
10
11/// `BoundedLinearCache` is a bounded cache with approximate key matching support.
12///
13/// The cache enforces a maximum capacity, and when the capacity is exceeded, the least recently used (LRU) element is evicted.
14///
15/// # Approximate Key Matching
16/// Keys must implement the `ApproxComparable` trait, which allows approximate equality comparisons based on the provided `tolerance`.
17/// This enables the cache to retrieve values even when the queried key is not an exact match but is "close enough."
18///
19/// # Example Usage
20/// ```
21/// use proximipy::caching::bounded::bounded_linear_cache::BoundedLinearCache;
22/// use proximipy::caching::approximate_cache::ApproximateCache;
23///
24/// let mut cache = BoundedLinearCache::new(3, 2.0);
25///
26/// cache.insert(10 as i16, "Value 1");
27/// cache.insert(20, "Value 2");
28/// cache.insert(30, "Value 3");
29///
30/// assert_eq!(cache.find(&11), Some("Value 1"));
31/// assert_eq!(cache.len(), 3);
32///
33/// cache.insert(40, "Value 4"); // Evicts the least recently used (Key(20))
34/// assert!(cache.find(&20).is_none());
35/// ```
36///
37/// # Type Parameters
38/// - `K`: The type of the keys, which must implement `ApproxComparable`, `Eq`, `Hash`, and `Clone`.
39/// - `V`: The type of the values, which must implement `Clone`.
40///
41/// # Methods
42/// - `new(max_capacity: usize, tolerance: f32) -> Self`: Creates a new `BoundedLinearCache` with the specified maximum capacity and tolerance.
43/// - `find(&mut self, key: &K) -> Option<V>`: Attempts to find a value matching the given key approximately. Promotes the found key to the head of the list.
44/// - `insert(&mut self, key: K, value: V)`: Inserts a key-value pair into the cache. Evicts the least recently used item if the cache is full.
45/// - `len(&self) -> usize`: Returns the current size of the cache.
46pub struct BoundedLinearCache<K, V> {
47    max_capacity: usize,
48    map: HashMap<K, SharedNode<K, V>>,
49    list: DoublyLinkedList<K, V>,
50    tolerance: f32,
51}
52
53impl<K, V> ApproximateCache<K, V> for BoundedLinearCache<K, V>
54where
55    K: ApproxComparable + Eq + Hash + Clone,
56    V: Clone,
57{
58    fn find(&mut self, key: &K) -> Option<V> {
59        let matching = self
60            .map
61            .keys()
62            .find(|&k| key.roughly_matches(k, self.tolerance))?;
63        let node: SharedNode<K, V> = self.map.get(matching).cloned()?;
64        self.list.remove(node.clone());
65        self.list.add_to_head(node.clone());
66        return Some(node.borrow().value.clone());
67    }
68
69    fn insert(&mut self, key: K, value: V) {
70        if self.len() == self.max_capacity {
71            if let Some(tail) = self.list.remove_tail() {
72                self.map.remove(&tail.borrow().key);
73            }
74        }
75        let new_node = Node::new(key.clone(), value);
76        self.list.add_to_head(new_node.clone());
77        self.map.insert(key, new_node);
78    }
79
80    fn len(&self) -> usize {
81        self.map.len()
82    }
83}
84
85impl<K, V> BoundedLinearCache<K, V> {
86    pub fn new(max_capacity: usize, tolerance: f32) -> Self {
87        assert!(max_capacity > 0);
88        assert!(tolerance > 0.0);
89        Self {
90            max_capacity,
91            map: HashMap::new(),
92            list: DoublyLinkedList::new(),
93            tolerance,
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    const TEST_TOLERANCE: f32 = 1e-8;
103    #[test]
104    fn test_lru_cache_basic_operations() {
105        let mut cache: BoundedLinearCache<i16, i16> = BoundedLinearCache::new(2, TEST_TOLERANCE);
106        cache.insert(1, 1); // Cache is {1=1}
107        cache.insert(2, 2); // Cache is {1=1, 2=2}
108        assert_eq!(cache.find(&1), Some(1)); // Returns 1, Cache is {2=2, 1=1}
109        cache.insert(3, 3); // Evicts key 2, Cache is {1=1, 3=3}
110        assert_eq!(cache.find(&2), None); // Key 2 not found
111        cache.insert(4, 4); // Evicts key 1, Cache is {3=3, 4=4}
112        assert_eq!(cache.find(&1), None); // Key 1 not found
113        assert_eq!(cache.find(&3), Some(3)); // Returns 3
114        assert_eq!(cache.find(&4), Some(4)); // Returns 4
115    }
116
117    #[test]
118    fn test_lru_cache_eviction_order() {
119        let mut cache: BoundedLinearCache<i16, i16> = BoundedLinearCache::new(3, TEST_TOLERANCE);
120        cache.insert(1, 1); // Cache is {1=1}
121        cache.insert(2, 2); // Cache is {1=1, 2=2}
122        cache.insert(3, 3); // Cache is {1=1, 2=2, 3=3}
123        cache.find(&1); // Access key 1, Cache is {2=2, 3=3, 1=1}
124        cache.insert(4, 4); // Evicts key 2, Cache is {3=3, 1=1, 4=4}
125        assert_eq!(cache.find(&2), None); // Key 2 not found
126        assert_eq!(cache.find(&3), Some(3)); // Returns 3
127        assert_eq!(cache.find(&4), Some(4)); // Returns 4
128        assert_eq!(cache.find(&1), Some(1)); // Returns 1
129    }
130
131    #[test]
132    fn test_lru_cache_overwrite() {
133        let mut cache: BoundedLinearCache<i16, i16> = BoundedLinearCache::new(2, TEST_TOLERANCE);
134        cache.insert(1, 1); // Cache is {1=1}
135        cache.insert(2, 2); // Cache is {1=1, 2=2}
136        cache.insert(1, 10); // Overwrites key 1, Cache is {2=2, 1=10}
137        assert_eq!(cache.find(&1), Some(10)); // Returns 10
138        cache.insert(3, 3); // Evicts key 2, Cache is {1=10, 3=3}
139        assert_eq!(cache.find(&2), None); // Key 2 not found
140        assert_eq!(cache.find(&3), Some(3)); // Returns 3
141    }
142
143    #[test]
144    fn test_lru_cache_capacity_one() {
145        let mut cache: BoundedLinearCache<i16, i16> = BoundedLinearCache::new(1, TEST_TOLERANCE);
146        cache.insert(1, 1); // Cache is {1=1}
147        assert_eq!(cache.find(&1), Some(1)); // Returns 1
148        cache.insert(2, 2); // Evicts key 1, Cache is {2=2}
149        assert_eq!(cache.find(&1), None); // Key 1 not found
150        assert_eq!(cache.find(&2), Some(2)); // Returns 2
151    }
152
153    #[test]
154    #[should_panic]
155    fn test_lru_cache_empty() {
156        let _cache: BoundedLinearCache<i16, i16> = BoundedLinearCache::new(0, TEST_TOLERANCE);
157    }
158}