harmos_storage/signal/waveform/
series.rs1use std::borrow::Cow;
18
19use super::Summary;
23
24#[derive(Clone, Debug, PartialEq)]
37pub struct Series<'a> {
38 values: Cow<'a, [f64]>,
39 sorted: bool,
40}
41
42impl<'a> Series<'a> {
49 #[must_use]
50 pub fn new(values: &'a [f64]) -> Self {
51 let values =
52 if values.iter().all(|value| value.is_finite()) { Cow::Borrowed(values) } else { Cow::Owned(finite(values.iter().copied())) };
53
54 Self { values, sorted: false }
55 }
56
57 pub fn sort(&mut self) {
60 if !self.sorted {
61 self.values.to_mut().sort_by(f64::total_cmp);
62 self.sorted = true;
63 }
64 }
65
66 #[must_use]
67 pub fn values(&self) -> &[f64] {
68 &self.values
69 }
70
71 #[must_use]
72 pub fn summary(&self) -> Summary {
73 Summary::of(&self.values)
74 }
75
76 #[must_use]
77 pub fn count(&self) -> u64 {
78 self.values.len() as u64
79 }
80
81 #[must_use]
82 pub fn is_empty(&self) -> bool {
83 self.values.is_empty()
84 }
85
86 #[must_use]
87 pub fn sum(&self) -> f64 {
88 self.summary().sum()
89 }
90
91 #[must_use]
92 pub fn mean(&self) -> Option<f64> {
93 self.summary().mean()
94 }
95
96 #[must_use]
97 pub fn min(&self) -> Option<f64> {
98 self.summary().min()
99 }
100
101 #[must_use]
102 pub fn max(&self) -> Option<f64> {
103 self.summary().max()
104 }
105
106 #[must_use]
107 pub fn rms(&self) -> Option<f64> {
108 self.summary().rms()
109 }
110
111 #[must_use]
113 pub fn std(&self) -> Option<f64> {
114 self.summary().std()
115 }
116
117 #[must_use]
118 pub fn median(&self) -> Option<f64> {
119 self.percentile(50.0)
120 }
121
122 #[must_use]
127 pub fn percentile(&self, rank: f64) -> Option<f64> {
128 if self.values.is_empty() || !(0.0..=100.0).contains(&rank) {
129 return None;
130 }
131 let ordered: Cow<'_, [f64]> = if self.sorted {
132 Cow::Borrowed(&self.values)
133 } else {
134 let mut copy = self.values.to_vec();
135 copy.sort_by(f64::total_cmp);
136 Cow::Owned(copy)
137 };
138
139 let position = rank / 100.0 * (ordered.len() - 1) as f64;
140 let lower = position.floor() as usize;
141 let upper = position.ceil() as usize;
142
143 Some(between(ordered[lower], ordered[upper], position - lower as f64))
144 }
145
146 pub fn all(&self, mut holds: impl FnMut(f64) -> bool) -> bool {
149 self.values.iter().all(|value| holds(*value))
150 }
151
152 pub fn any(&self, mut holds: impl FnMut(f64) -> bool) -> bool {
153 self.values.iter().any(|value| holds(*value))
154 }
155
156 #[must_use]
157 pub fn all_below(&self, limit: f64) -> bool {
158 self.all(|value| value < limit)
159 }
160
161 #[must_use]
162 pub fn all_within(&self, min: f64, max: f64) -> bool {
163 self.all(|value| (min..=max).contains(&value))
164 }
165}
166
167impl<'a> From<&'a [f64]> for Series<'a> {
168 fn from(values: &'a [f64]) -> Self {
169 Self::new(values)
170 }
171}
172
173impl From<Vec<f64>> for Series<'static> {
174 fn from(mut values: Vec<f64>) -> Self {
175 values.retain(|value| value.is_finite());
176 Self { values: Cow::Owned(values), sorted: false }
177 }
178}
179
180impl FromIterator<f64> for Series<'static> {
181 fn from_iter<I: IntoIterator<Item = f64>>(values: I) -> Self {
182 Self { values: Cow::Owned(finite(values)), sorted: false }
183 }
184}
185
186fn finite(values: impl IntoIterator<Item = f64>) -> Vec<f64> {
191 values.into_iter().filter(|value| value.is_finite()).collect()
192}
193
194fn between(low: f64, high: f64, fraction: f64) -> f64 {
197 if fraction == 0.0 || low == high {
198 low
199 } else if low.signum() == high.signum() {
200 low + (high - low) * fraction
201 } else {
202 low * (1.0 - fraction) + high * fraction
203 }
204}
205
206#[cfg(test)]
209mod tests {
210 use super::Series;
211 use crate::signal::Summary;
212
213 const CLOSE: f64 = 1e-9;
214
215 fn close(found: Option<f64>, expected: f64) {
216 let found = found.expect("an answer");
217 assert!((found - expected).abs() <= CLOSE * expected.abs().max(1.0), "{found} is not {expected}");
218 }
219
220 #[test]
221 fn empty_series_undefined() {
222 let series = Series::new(&[]);
223
224 assert_eq!(series.count(), 0);
225 assert_eq!(series.sum(), 0.0);
226 assert_eq!(series.mean(), None);
227 assert_eq!(series.median(), None);
228 assert_eq!(series.min(), None);
229 assert_eq!(series.max(), None);
230 assert_eq!(series.rms(), None);
231 assert_eq!(series.std(), None);
232 assert_eq!(series.percentile(0.0), None);
233 assert_eq!(series.percentile(100.0), None);
234 assert!(series.all(|_| false), "no value refutes a predicate");
235 assert!(!series.any(|_| true));
236 }
237
238 #[test]
239 fn single_value_degenerates() {
240 let series = Series::new(&[-4.0]);
241
242 assert_eq!(series.count(), 1);
243 assert_eq!(series.sum(), -4.0);
244 assert_eq!(series.mean(), Some(-4.0));
245 assert_eq!(series.median(), Some(-4.0));
246 assert_eq!(series.min(), Some(-4.0));
247 assert_eq!(series.max(), Some(-4.0));
248 assert_eq!(series.rms(), Some(4.0));
249 assert_eq!(series.std(), Some(0.0));
250 assert_eq!(series.percentile(0.0), Some(-4.0));
251 assert_eq!(series.percentile(37.0), Some(-4.0));
252 assert_eq!(series.percentile(100.0), Some(-4.0));
253 }
254
255 #[test]
256 fn known_run_statistics() {
257 let series = Series::new(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
258
259 assert_eq!(series.count(), 8);
260 assert_eq!(series.sum(), 40.0);
261 close(series.mean(), 5.0);
262 assert_eq!(series.min(), Some(2.0));
263 assert_eq!(series.max(), Some(9.0));
264 close(series.std(), 2.0);
265 close(series.rms(), (232.0_f64 / 8.0).sqrt());
266 }
267
268 #[test]
269 fn odd_even_medians() {
270 assert_eq!(Series::new(&[9.0, 1.0, 5.0]).median(), Some(5.0));
271 assert_eq!(Series::new(&[9.0, 1.0, 5.0, 3.0]).median(), Some(4.0));
272 }
273
274 #[test]
275 fn percentiles_interpolate_ranks() {
276 let series = Series::new(&[40.0, 10.0, 30.0, 20.0]);
277
278 assert_eq!(series.percentile(0.0), Some(10.0));
279 assert_eq!(series.percentile(100.0), Some(40.0));
280 close(series.percentile(50.0), 25.0);
281 close(series.percentile(25.0), 17.5);
282 close(series.percentile(90.0), 37.0);
283 assert_eq!(series.percentile(100.5), None);
284 assert_eq!(series.percentile(-1.0), None);
285 assert_eq!(series.percentile(f64::NAN), None);
286 }
287
288 #[test]
289 fn sorting_preserves_percentiles() {
290 let values = [3.0, -1.0, 8.0, 2.5, 0.0];
291 let unsorted = Series::new(&values);
292 let mut sorted = Series::new(&values);
293 sorted.sort();
294
295 for rank in [0.0, 12.5, 50.0, 77.0, 100.0] {
296 assert_eq!(unsorted.percentile(rank), sorted.percentile(rank));
297 }
298 }
299
300 #[test]
301 fn extremes_avoid_overflow() {
302 let series = Series::new(&[-f64::MAX, f64::MAX]);
303
304 assert_eq!(series.median(), Some(0.0));
305 assert_eq!(Series::new(&[f64::MAX, f64::MAX]).mean(), Some(f64::MAX));
306 }
307
308 #[test]
309 fn non_finite_skipped() {
310 let values = [1.0, f64::NAN, 3.0, f64::INFINITY, f64::NEG_INFINITY];
311 let series = Series::new(&values);
312 let collected: Series<'static> = values.iter().copied().collect();
313 let owned = Series::from(values.to_vec());
314
315 for series in [&series, &collected, &owned] {
316 assert_eq!(series.values(), &[1.0, 3.0]);
317 assert_eq!(series.count(), 2);
318 assert_eq!(series.median(), Some(2.0));
319 assert_eq!(series.max(), Some(3.0));
320 }
321 assert_eq!(Summary::of(&values), Summary::of(&[1.0, 3.0]));
322 }
323
324 #[test]
325 fn predicates_bound_inclusively() {
326 let series = Series::new(&[1.0, 2.5, 4.0]);
327
328 assert!(series.all_below(4.5));
329 assert!(!series.all_below(4.0), "the limit itself is not below");
330 assert!(series.all_within(1.0, 4.0), "the bounds are inclusive");
331 assert!(!series.all_within(1.5, 4.0));
332 assert!(series.any(|value| value > 3.0));
333 assert!(series.all(|value| value > 0.0));
334 }
335}