1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5use crate::Error;
6use crate::validation::valid_nonempty_text;
7
8#[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 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
57pub 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 #[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 pub fn values_mut(&mut self) -> &mut [T] {
126 Arc::make_mut(&mut self.values).as_mut_slice()
127 }
128
129 pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
131 Arc::make_mut(&mut self.values).get_mut(index)
132 }
133
134 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 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 #[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}