powerio_core/
time_series.rs1use 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<[TimePoint]>,
60 values: Arc<[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: time_points.into(),
77 values: values.into(),
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<(&TimePoint, &T)> {
93 Some((self.time_points.get(index)?, self.values.get(index)?))
94 }
95
96 #[must_use]
97 pub fn time_point(&self, index: usize) -> Option<&TimePoint> {
98 self.time_points.get(index)
99 }
100
101 #[must_use]
102 pub fn value(&self, index: usize) -> Option<&T> {
103 self.values.get(index)
104 }
105
106 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&TimePoint, &T)> {
107 self.time_points.iter().zip(self.values.iter())
108 }
109
110 #[must_use]
111 pub fn len(&self) -> usize {
112 self.values.len()
113 }
114
115 #[must_use]
116 pub fn is_empty(&self) -> bool {
117 self.values.is_empty()
118 }
119}
120
121impl<T> Clone for TimeSeries<T> {
122 fn clone(&self) -> Self {
123 Self {
124 time_points: Arc::clone(&self.time_points),
125 values: Arc::clone(&self.values),
126 }
127 }
128}
129
130impl<T: fmt::Debug> fmt::Debug for TimeSeries<T> {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 formatter
133 .debug_struct("TimeSeries")
134 .field("time_points", &self.time_points)
135 .field("values", &self.values)
136 .finish()
137 }
138}
139
140#[cfg_attr(
142 not(test),
143 expect(dead_code, reason = "used by later series constructors")
144)]
145pub(crate) fn checked_dimension_product(
146 what: &str,
147 rows: usize,
148 columns: usize,
149) -> Result<usize, Error> {
150 rows.checked_mul(columns).ok_or_else(|| {
151 Error::new(
152 &crate::codes::VALIDATE_TIME_SERIES_DIMENSION_OVERFLOW,
153 format!("{what} dimensions {rows} by {columns} overflow usize"),
154 )
155 })
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn labels_and_exact_duration_parts_are_checked() {
164 assert!(TimePoint::new("", None).is_err());
165 assert!(TimePoint::new("x".repeat(65_537), None).is_err());
166 assert!(TimePoint::from_duration_parts("t0", u64::MAX, 999_999_999).is_ok());
167 assert!(TimePoint::from_duration_parts("t0", 0, 1_000_000_000).is_err());
168 }
169
170 #[test]
171 fn shape_and_dimension_overflow_are_errors() {
172 let point = TimePoint::new("t0", None).unwrap();
173 assert!(TimeSeries::<u8>::new(vec![point], Vec::new()).is_err());
174 assert!(checked_dimension_product("column", usize::MAX, 2).is_err());
175 }
176
177 #[test]
178 fn lookup_and_iteration_preserve_value_identity() {
179 let points = vec![
180 TimePoint::new("t0", Some(Duration::from_secs(1))).unwrap(),
181 TimePoint::new("t1", Some(Duration::from_secs(2))).unwrap(),
182 ];
183 let values = vec![String::from("a"), String::from("b")];
184 let series = TimeSeries::new(points, values).unwrap();
185 let value_pointer = series.value(1).unwrap().as_ptr();
186 assert_eq!(series.get(1).unwrap().0.label(), "t1");
187 assert_eq!(series.iter().count(), 2);
188 assert_eq!(series.value(1).unwrap().as_ptr(), value_pointer);
189 }
190}