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 {
177 values: Cow::Owned(values),
178 sorted: false,
179 }
180 }
181}
182
183impl FromIterator<f64> for Series<'static> {
184 fn from_iter<I: IntoIterator<Item = f64>>(values: I) -> Self {
185 Self {
186 values: Cow::Owned(finite(values)),
187 sorted: false,
188 }
189 }
190}
191
192fn finite(values: impl IntoIterator<Item = f64>) -> Vec<f64> {
197 values.into_iter().filter(|value| value.is_finite()).collect()
198}
199
200fn between(low: f64, high: f64, fraction: f64) -> f64 {
203 if fraction == 0.0 || low == high {
204 low
205 } else if low.signum() == high.signum() {
206 low + (high - low) * fraction
207 } else {
208 low * (1.0 - fraction) + high * fraction
209 }
210}
211
212#[cfg(test)]
215mod tests {
216 use super::Series;
217 use crate::signal::Summary;
218
219 const CLOSE: f64 = 1e-9;
220
221 fn close(found: Option<f64>, expected: f64) {
222 let found = found.expect("an answer");
223 assert!((found - expected).abs() <= CLOSE * expected.abs().max(1.0), "{found} is not {expected}");
224 }
225
226 #[test]
227 fn empty_series_undefined() {
228 let series = Series::new(&[]);
229
230 assert_eq!(series.count(), 0);
231 assert_eq!(series.sum(), 0.0);
232 assert_eq!(series.mean(), None);
233 assert_eq!(series.median(), None);
234 assert_eq!(series.min(), None);
235 assert_eq!(series.max(), None);
236 assert_eq!(series.rms(), None);
237 assert_eq!(series.std(), None);
238 assert_eq!(series.percentile(0.0), None);
239 assert_eq!(series.percentile(100.0), None);
240 assert!(series.all(|_| false), "no value refutes a predicate");
241 assert!(!series.any(|_| true));
242 }
243
244 #[test]
245 fn single_value_degenerates() {
246 let series = Series::new(&[-4.0]);
247
248 assert_eq!(series.count(), 1);
249 assert_eq!(series.sum(), -4.0);
250 assert_eq!(series.mean(), Some(-4.0));
251 assert_eq!(series.median(), Some(-4.0));
252 assert_eq!(series.min(), Some(-4.0));
253 assert_eq!(series.max(), Some(-4.0));
254 assert_eq!(series.rms(), Some(4.0));
255 assert_eq!(series.std(), Some(0.0));
256 assert_eq!(series.percentile(0.0), Some(-4.0));
257 assert_eq!(series.percentile(37.0), Some(-4.0));
258 assert_eq!(series.percentile(100.0), Some(-4.0));
259 }
260
261 #[test]
262 fn known_run_statistics() {
263 let series = Series::new(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
264
265 assert_eq!(series.count(), 8);
266 assert_eq!(series.sum(), 40.0);
267 close(series.mean(), 5.0);
268 assert_eq!(series.min(), Some(2.0));
269 assert_eq!(series.max(), Some(9.0));
270 close(series.std(), 2.0);
271 close(series.rms(), (232.0_f64 / 8.0).sqrt());
272 }
273
274 #[test]
275 fn odd_even_medians() {
276 assert_eq!(Series::new(&[9.0, 1.0, 5.0]).median(), Some(5.0));
277 assert_eq!(Series::new(&[9.0, 1.0, 5.0, 3.0]).median(), Some(4.0));
278 }
279
280 #[test]
281 fn percentiles_interpolate_ranks() {
282 let series = Series::new(&[40.0, 10.0, 30.0, 20.0]);
283
284 assert_eq!(series.percentile(0.0), Some(10.0));
285 assert_eq!(series.percentile(100.0), Some(40.0));
286 close(series.percentile(50.0), 25.0);
287 close(series.percentile(25.0), 17.5);
288 close(series.percentile(90.0), 37.0);
289 assert_eq!(series.percentile(100.5), None);
290 assert_eq!(series.percentile(-1.0), None);
291 assert_eq!(series.percentile(f64::NAN), None);
292 }
293
294 #[test]
295 fn sorting_preserves_percentiles() {
296 let values = [3.0, -1.0, 8.0, 2.5, 0.0];
297 let unsorted = Series::new(&values);
298 let mut sorted = Series::new(&values);
299 sorted.sort();
300
301 for rank in [0.0, 12.5, 50.0, 77.0, 100.0] {
302 assert_eq!(unsorted.percentile(rank), sorted.percentile(rank));
303 }
304 }
305
306 #[test]
307 fn extremes_avoid_overflow() {
308 let series = Series::new(&[-f64::MAX, f64::MAX]);
309
310 assert_eq!(series.median(), Some(0.0));
311 assert_eq!(Series::new(&[f64::MAX, f64::MAX]).mean(), Some(f64::MAX));
312 }
313
314 #[test]
315 fn non_finite_skipped() {
316 let values = [1.0, f64::NAN, 3.0, f64::INFINITY, f64::NEG_INFINITY];
317 let series = Series::new(&values);
318 let collected: Series<'static> = values.iter().copied().collect();
319 let owned = Series::from(values.to_vec());
320
321 for series in [&series, &collected, &owned] {
322 assert_eq!(series.values(), &[1.0, 3.0]);
323 assert_eq!(series.count(), 2);
324 assert_eq!(series.median(), Some(2.0));
325 assert_eq!(series.max(), Some(3.0));
326 }
327 assert_eq!(Summary::of(&values), Summary::of(&[1.0, 3.0]));
328 }
329
330 #[test]
331 fn predicates_bound_inclusively() {
332 let series = Series::new(&[1.0, 2.5, 4.0]);
333
334 assert!(series.all_below(4.5));
335 assert!(!series.all_below(4.0), "the limit itself is not below");
336 assert!(series.all_within(1.0, 4.0), "the bounds are inclusive");
337 assert!(!series.all_within(1.5, 4.0));
338 assert!(series.any(|value| value > 3.0));
339 assert!(series.all(|value| value > 0.0));
340 }
341}