Skip to main content

libspot_rs/
ubend.rs

1//! Circular buffer implementation (Ubend)
2//!
3//! This module implements a circular buffer that matches the C implementation exactly.
4//! The Ubend structure is a kind of circular vector that starts empty, fills up to capacity,
5//! and then overwrites older data with newer data.
6
7use crate::error::{SpotError, SpotResult};
8
9/// Circular buffer implementation that matches the C Ubend structure
10///
11/// # Serialization
12///
13/// When the `serde` feature is enabled, this struct can be serialized and deserialized.
14/// This is useful for saving the state of a SPOT detector and restoring it later.
15#[derive(Debug, Clone)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct Ubend {
18    /// Current position inside the container
19    cursor: usize,
20    /// Maximum storage capacity
21    capacity: usize,
22    /// Last erased value (i.e., replaced by a new one)
23    #[cfg_attr(feature = "serde", serde(with = "crate::ser::nan_safe_f64"))]
24    last_erased_data: f64,
25    /// Container fill status (true = filled, false = not filled)
26    filled: bool,
27    /// Data container
28    data: Vec<f64>,
29}
30
31impl Ubend {
32    /// Initialize a new Ubend with the given capacity
33    pub fn new(capacity: usize) -> SpotResult<Self> {
34        if capacity == 0 {
35            return Err(SpotError::MemoryAllocationFailed);
36        }
37
38        Ok(Self {
39            cursor: 0,
40            filled: false,
41            capacity,
42            last_erased_data: f64::NAN,
43            data: vec![0.0; capacity],
44        })
45    }
46
47    /// Get the current size of the container
48    /// Returns capacity if filled, otherwise returns cursor position
49    pub fn size(&self) -> usize {
50        if self.filled {
51            self.capacity
52        } else {
53            self.cursor
54        }
55    }
56
57    /// Reset the container to its empty state, keeping the allocated buffer.
58    ///
59    /// After `reset`, [`size`](Self::size) returns 0 and the next [`push`](Self::push)
60    /// behaves as on a freshly constructed container. The underlying `Vec`
61    /// allocation is preserved (no realloc).
62    pub(crate) fn reset(&mut self) {
63        self.cursor = 0;
64        self.filled = false;
65        self.last_erased_data = f64::NAN;
66    }
67
68    /// Push a new value into the container
69    /// Returns the value that was erased (if any), otherwise NaN
70    pub fn push(&mut self, x: f64) -> f64 {
71        // If the container has already been filled, we must keep in memory
72        // the data we will erase
73        if self.filled {
74            self.last_erased_data = self.data[self.cursor];
75        }
76
77        // Assign value at cursor
78        self.data[self.cursor] = x;
79
80        // Increment cursor
81        if self.cursor == self.capacity - 1 {
82            self.cursor = 0;
83            self.filled = true;
84        } else {
85            self.cursor += 1;
86        }
87
88        self.last_erased_data
89    }
90
91    /// Get iterator over the data in insertion order
92    pub fn iter(&self) -> UbendIterator<'_> {
93        UbendIterator {
94            ubend: self,
95            index: 0,
96        }
97    }
98
99    /// Get the data at a specific index in insertion order
100    pub fn get(&self, index: usize) -> Option<f64> {
101        let size = self.size();
102        if index >= size {
103            return None;
104        }
105
106        if !self.filled {
107            // Simple case: data is contiguous from 0 to cursor-1
108            Some(self.data[index])
109        } else {
110            // Complex case: data wraps around
111            let real_index = (self.cursor + index) % self.capacity;
112            Some(self.data[real_index])
113        }
114    }
115
116    /// Access to raw data (for compatibility with C implementation)
117    pub fn raw_data(&self) -> &[f64] {
118        &self.data
119    }
120
121    /// Get capacity
122    pub fn capacity(&self) -> usize {
123        self.capacity
124    }
125
126    /// Check if the buffer is filled
127    pub fn is_filled(&self) -> bool {
128        self.filled
129    }
130
131    /// Get current cursor position
132    pub fn cursor(&self) -> usize {
133        self.cursor
134    }
135
136    /// Get last erased data
137    pub fn last_erased_data(&self) -> f64 {
138        self.last_erased_data
139    }
140
141    /// Get all data in insertion order as a vector
142    pub fn data(&self) -> Vec<f64> {
143        self.iter().collect()
144    }
145}
146
147/// Iterator over Ubend data in insertion order
148pub struct UbendIterator<'a> {
149    ubend: &'a Ubend,
150    index: usize,
151}
152
153impl<'a> Iterator for UbendIterator<'a> {
154    type Item = f64;
155
156    fn next(&mut self) -> Option<Self::Item> {
157        let result = self.ubend.get(self.index);
158        self.index += 1;
159        result
160    }
161}
162
163impl<'a> ExactSizeIterator for UbendIterator<'a> {
164    fn len(&self) -> usize {
165        self.ubend.size()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use approx::assert_relative_eq;
173
174    #[test]
175    fn test_ubend_reset_clears_state_and_preserves_capacity() {
176        let mut ub = Ubend::new(3).unwrap();
177        // Fill past capacity so `filled = true` and `last_erased_data` is set.
178        let _ = ub.push(1.0);
179        let _ = ub.push(2.0);
180        let _ = ub.push(3.0);
181        let erased = ub.push(4.0);
182        assert_relative_eq!(erased, 1.0);
183        assert_eq!(ub.size(), 3);
184
185        ub.reset();
186
187        // Empty again, but capacity (and the underlying Vec) is preserved.
188        assert_eq!(ub.size(), 0);
189        assert_eq!(ub.capacity, 3);
190        assert_eq!(ub.cursor, 0);
191        assert!(!ub.filled);
192        assert!(ub.last_erased_data.is_nan());
193        assert_eq!(ub.data.len(), 3); // Vec not reallocated
194
195        // After reset, the next push behaves like on a fresh Ubend:
196        // it returns NaN (nothing erased) instead of the old `last_erased_data`.
197        let erased_after_reset = ub.push(10.0);
198        assert!(erased_after_reset.is_nan());
199        assert_eq!(ub.size(), 1);
200    }
201
202    #[test]
203    fn test_ubend_reset_is_idempotent() {
204        let mut ub = Ubend::new(2).unwrap();
205        ub.reset();
206        ub.reset();
207        assert_eq!(ub.size(), 0);
208        assert!(ub.last_erased_data.is_nan());
209    }
210
211    #[test]
212    fn test_ubend_creation() {
213        let ubend = Ubend::new(5).unwrap();
214        assert_eq!(ubend.capacity(), 5);
215        assert_eq!(ubend.size(), 0);
216        assert!(!ubend.is_filled());
217        assert_eq!(ubend.cursor(), 0);
218        assert!(ubend.last_erased_data().is_nan());
219    }
220
221    #[test]
222    fn test_ubend_zero_capacity() {
223        let result = Ubend::new(0);
224        assert!(result.is_err());
225        assert_eq!(result.unwrap_err(), SpotError::MemoryAllocationFailed);
226    }
227
228    #[test]
229    fn test_ubend_push_before_full() {
230        let mut ubend = Ubend::new(3).unwrap();
231
232        // Push first element
233        let erased = ubend.push(1.0);
234        assert!(erased.is_nan());
235        assert_eq!(ubend.size(), 1);
236        assert!(!ubend.is_filled());
237        assert_eq!(ubend.cursor(), 1);
238
239        // Push second element
240        let erased = ubend.push(2.0);
241        assert!(erased.is_nan());
242        assert_eq!(ubend.size(), 2);
243        assert!(!ubend.is_filled());
244        assert_eq!(ubend.cursor(), 2);
245
246        // Push third element
247        let erased = ubend.push(3.0);
248        assert!(erased.is_nan());
249        assert_eq!(ubend.size(), 3);
250        assert!(ubend.is_filled());
251        assert_eq!(ubend.cursor(), 0);
252    }
253
254    #[test]
255    fn test_ubend_push_after_full() {
256        let mut ubend = Ubend::new(3).unwrap();
257
258        // Fill the buffer
259        ubend.push(1.0);
260        ubend.push(2.0);
261        ubend.push(3.0);
262
263        // Now it should start overwriting
264        let erased = ubend.push(4.0);
265        assert_relative_eq!(erased, 1.0);
266        assert_eq!(ubend.size(), 3);
267        assert!(ubend.is_filled());
268        assert_eq!(ubend.cursor(), 1);
269
270        let erased = ubend.push(5.0);
271        assert_relative_eq!(erased, 2.0);
272        assert_eq!(ubend.size(), 3);
273        assert!(ubend.is_filled());
274        assert_eq!(ubend.cursor(), 2);
275    }
276
277    #[test]
278    fn test_ubend_get() {
279        let mut ubend = Ubend::new(3).unwrap();
280
281        // Test empty buffer
282        assert!(ubend.get(0).is_none());
283
284        // Add some data
285        ubend.push(10.0);
286        ubend.push(20.0);
287
288        assert_relative_eq!(ubend.get(0).unwrap(), 10.0);
289        assert_relative_eq!(ubend.get(1).unwrap(), 20.0);
290        assert!(ubend.get(2).is_none());
291
292        // Fill buffer and test wraparound
293        ubend.push(30.0);
294        ubend.push(40.0); // This should overwrite 10.0
295
296        assert_relative_eq!(ubend.get(0).unwrap(), 20.0);
297        assert_relative_eq!(ubend.get(1).unwrap(), 30.0);
298        assert_relative_eq!(ubend.get(2).unwrap(), 40.0);
299    }
300
301    #[test]
302    fn test_ubend_iterator() {
303        let mut ubend = Ubend::new(3).unwrap();
304
305        ubend.push(1.0);
306        ubend.push(2.0);
307        ubend.push(3.0);
308
309        let values: Vec<f64> = ubend.iter().collect();
310        assert_eq!(values, vec![1.0, 2.0, 3.0]);
311
312        // Test after wraparound
313        ubend.push(4.0);
314        let values: Vec<f64> = ubend.iter().collect();
315        assert_eq!(values, vec![2.0, 3.0, 4.0]);
316    }
317
318    #[test]
319    fn test_ubend_exact_size_iterator() {
320        let mut ubend = Ubend::new(3).unwrap();
321
322        assert_eq!(ubend.iter().len(), 0);
323
324        ubend.push(1.0);
325        assert_eq!(ubend.iter().len(), 1);
326
327        ubend.push(2.0);
328        ubend.push(3.0);
329        assert_eq!(ubend.iter().len(), 3);
330
331        ubend.push(4.0);
332        assert_eq!(ubend.iter().len(), 3);
333    }
334}