Skip to main content

powerio_core/
time_series.rs

1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5use crate::Error;
6use crate::validation::valid_nonempty_text;
7
8/// One ordered position in a time series.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct TimePoint {
11    label: Box<str>,
12    duration: Option<Duration>,
13}
14
15impl TimePoint {
16    pub fn new(label: impl Into<String>, duration: Option<Duration>) -> Result<Self, Error> {
17        let label = label.into();
18        if !valid_nonempty_text(&label) {
19            return Err(Error::new(
20                &crate::codes::VALIDATE_TIME_POINT_INVALID_LABEL,
21                "a time point label must be nonempty and bounded",
22            ));
23        }
24        Ok(Self {
25            label: label.into_boxed_str(),
26            duration,
27        })
28    }
29
30    /// Construct an exact stored duration, rejecting a normalized nanosecond
31    /// value instead of silently carrying into the seconds field.
32    pub fn from_duration_parts(
33        label: impl Into<String>,
34        seconds: u64,
35        nanoseconds: u32,
36    ) -> Result<Self, Error> {
37        if nanoseconds >= 1_000_000_000 {
38            return Err(Error::new(
39                &crate::codes::VALIDATE_TIME_POINT_INVALID_DURATION,
40                format!("duration nanosecond remainder {nanoseconds} is at least one billion"),
41            ));
42        }
43        Self::new(label, Some(Duration::new(seconds, nanoseconds)))
44    }
45
46    #[must_use]
47    pub fn label(&self) -> &str {
48        &self.label
49    }
50
51    #[must_use]
52    pub const fn duration(&self) -> Option<Duration> {
53        self.duration
54    }
55}
56
57/// Ordered complete values of one Rust type.
58pub struct TimeSeries<T> {
59    time_points: Arc<Vec<TimePoint>>,
60    values: Arc<Vec<T>>,
61}
62
63impl<T> TimeSeries<T> {
64    pub fn new(time_points: Vec<TimePoint>, values: Vec<T>) -> Result<Self, Error> {
65        if time_points.len() != values.len() {
66            return Err(Error::new(
67                &crate::codes::VALIDATE_TIME_SERIES_SHAPE,
68                format!(
69                    "time series has {} values for {} time points",
70                    values.len(),
71                    time_points.len()
72                ),
73            ));
74        }
75        Ok(Self {
76            time_points: Arc::new(time_points),
77            values: Arc::new(values),
78        })
79    }
80
81    #[must_use]
82    pub fn time_points(&self) -> &[TimePoint] {
83        &self.time_points
84    }
85
86    #[must_use]
87    pub fn values(&self) -> &[T] {
88        &self.values
89    }
90
91    #[must_use]
92    pub fn get(&self, index: usize) -> Option<&T> {
93        self.values.get(index)
94    }
95
96    /// Look up one complete entry when its time metadata is also needed.
97    #[must_use]
98    pub fn entry(&self, index: usize) -> Option<(&TimePoint, &T)> {
99        Some((self.time_points.get(index)?, self.values.get(index)?))
100    }
101
102    #[must_use]
103    pub fn time_point(&self, index: usize) -> Option<&TimePoint> {
104        self.time_points.get(index)
105    }
106
107    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&TimePoint, &T)> {
108        self.time_points.iter().zip(self.values.iter())
109    }
110
111    #[must_use]
112    pub fn len(&self) -> usize {
113        self.values.len()
114    }
115
116    #[must_use]
117    pub fn is_empty(&self) -> bool {
118        self.values.is_empty()
119    }
120}
121
122impl<T: Clone> TimeSeries<T> {
123    /// Mutably borrow all values through copy on write. Time metadata remains
124    /// shared because editing a value never changes its position.
125    pub fn values_mut(&mut self) -> &mut [T] {
126        Arc::make_mut(&mut self.values).as_mut_slice()
127    }
128
129    /// Mutably borrow one value through copy on write.
130    pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
131        Arc::make_mut(&mut self.values).get_mut(index)
132    }
133
134    /// Mutably borrow one value together with its immutable time metadata.
135    pub fn entry_mut(&mut self, index: usize) -> Option<(&TimePoint, &mut T)> {
136        let time_point = self.time_points.get(index)?;
137        let value = Arc::make_mut(&mut self.values).get_mut(index)?;
138        Some((time_point, value))
139    }
140
141    /// Iterate over mutable values and immutable time metadata through one
142    /// copy on write detachment.
143    pub fn iter_mut(&mut self) -> impl ExactSizeIterator<Item = (&TimePoint, &mut T)> {
144        self.time_points
145            .iter()
146            .zip(Arc::make_mut(&mut self.values).iter_mut())
147    }
148
149    /// Consume the series and map each complete value while retaining the
150    /// time axis. A uniquely owned series moves its values into `map` without
151    /// cloning them; a shared series first performs the copy on write split.
152    #[must_use]
153    pub fn map_values<U>(self, map: impl FnMut(T) -> U) -> TimeSeries<U> {
154        let values = Arc::unwrap_or_clone(self.values)
155            .into_iter()
156            .map(map)
157            .collect();
158        TimeSeries {
159            time_points: self.time_points,
160            values: Arc::new(values),
161        }
162    }
163}
164
165impl<T> Clone for TimeSeries<T> {
166    fn clone(&self) -> Self {
167        Self {
168            time_points: Arc::clone(&self.time_points),
169            values: Arc::clone(&self.values),
170        }
171    }
172}
173
174impl<T: fmt::Debug> fmt::Debug for TimeSeries<T> {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        formatter
177            .debug_struct("TimeSeries")
178            .field("time_points", &self.time_points)
179            .field("values", &self.values)
180            .finish()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn labels_and_exact_duration_parts_are_checked() {
190        assert!(TimePoint::new("", None).is_err());
191        assert!(TimePoint::new("x".repeat(65_537), None).is_err());
192        assert!(TimePoint::from_duration_parts("t0", u64::MAX, 999_999_999).is_ok());
193        assert!(TimePoint::from_duration_parts("t0", 0, 1_000_000_000).is_err());
194    }
195
196    #[test]
197    fn shape_and_dimension_overflow_are_errors() {
198        let point = TimePoint::new("t0", None).unwrap();
199        assert!(TimeSeries::<u8>::new(vec![point], Vec::new()).is_err());
200    }
201
202    #[test]
203    fn lookup_and_iteration_preserve_value_identity() {
204        let points = vec![
205            TimePoint::new("t0", Some(Duration::from_secs(1))).unwrap(),
206            TimePoint::new("t1", Some(Duration::from_secs(2))).unwrap(),
207        ];
208        let values = vec![String::from("a"), String::from("b")];
209        let series = TimeSeries::new(points, values).unwrap();
210        let value_pointer = series.get(1).unwrap().as_ptr();
211        assert_eq!(series.entry(1).unwrap().0.label(), "t1");
212        assert_eq!(series.iter().count(), 2);
213        assert_eq!(series.get(1).unwrap().as_ptr(), value_pointer);
214    }
215
216    #[test]
217    fn mutable_access_is_copy_on_write_and_keeps_time_metadata() {
218        let points = vec![
219            TimePoint::new("t0", None).unwrap(),
220            TimePoint::new("t1", None).unwrap(),
221        ];
222        let mut edited = TimeSeries::new(points, vec![1_u8, 2]).unwrap();
223        let original = edited.clone();
224        assert!(Arc::ptr_eq(&edited.time_points, &original.time_points));
225        assert!(Arc::ptr_eq(&edited.values, &original.values));
226
227        *edited.get_mut(1).unwrap() = 9;
228        assert_eq!(edited.get(1), Some(&9));
229        assert_eq!(original.get(1), Some(&2));
230        assert!(Arc::ptr_eq(&edited.time_points, &original.time_points));
231        assert!(!Arc::ptr_eq(&edited.values, &original.values));
232
233        let (point, value) = edited.entry_mut(0).unwrap();
234        assert_eq!(point.label(), "t0");
235        *value = 7;
236        assert_eq!(edited.iter_mut().map(|(_, value)| *value).sum::<u8>(), 16);
237    }
238
239    #[test]
240    fn consuming_map_moves_unique_values_and_retains_the_axis() {
241        use std::sync::atomic::{AtomicUsize, Ordering};
242
243        static CLONES: AtomicUsize = AtomicUsize::new(0);
244
245        #[derive(Debug)]
246        struct Counted(u8);
247
248        impl Clone for Counted {
249            fn clone(&self) -> Self {
250                CLONES.fetch_add(1, Ordering::Relaxed);
251                Self(self.0)
252            }
253        }
254
255        CLONES.store(0, Ordering::Relaxed);
256        let series =
257            TimeSeries::new(vec![TimePoint::new("t0", None).unwrap()], vec![Counted(3)]).unwrap();
258        let mapped = series.map_values(|value| usize::from(value.0));
259        assert_eq!(mapped.get(0), Some(&3));
260        assert_eq!(mapped.entry(0).unwrap().0.label(), "t0");
261        assert_eq!(CLONES.load(Ordering::Relaxed), 0);
262    }
263}