brightdate/
comparisons.rs1use crate::types::BrightDateValue;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, PartialEq)]
8pub struct Stats {
9 pub count: usize,
10 pub min: BrightDateValue,
11 pub max: BrightDateValue,
12 pub range: BrightDateValue,
13 pub mean: BrightDateValue,
14 pub median: BrightDateValue,
15 pub std_dev: BrightDateValue,
16}
17
18pub fn closest(target: BrightDateValue, values: &[BrightDateValue]) -> Option<BrightDateValue> {
20 values
21 .iter()
22 .copied()
23 .min_by(|a, b| {
24 let da = (a - target).abs();
25 let db = (b - target).abs();
26 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
27 })
28}
29
30pub fn within(
32 target: BrightDateValue,
33 values: &[BrightDateValue],
34 max_distance: f64,
35) -> Vec<BrightDateValue> {
36 values
37 .iter()
38 .copied()
39 .filter(|&v| (v - target).abs() <= max_distance)
40 .collect()
41}
42
43pub fn partition(
49 values: &[BrightDateValue],
50 reference: BrightDateValue,
51) -> (Vec<BrightDateValue>, Vec<BrightDateValue>) {
52 let past: Vec<_> = values.iter().copied().filter(|&v| v < reference).collect();
53 let future: Vec<_> = values.iter().copied().filter(|&v| v >= reference).collect();
54 (past, future)
55}
56
57pub fn group_by_day(values: &[BrightDateValue]) -> HashMap<i64, Vec<BrightDateValue>> {
59 let mut map: HashMap<i64, Vec<BrightDateValue>> = HashMap::new();
60 for &v in values {
61 map.entry(v.floor() as i64).or_default().push(v);
62 }
63 map
64}
65
66pub fn statistics(values: &[BrightDateValue]) -> Stats {
68 assert!(!values.is_empty(), "Cannot compute statistics of empty array");
69
70 let count = values.len();
71 let sum: f64 = values.iter().sum();
72 let mean = sum / count as f64;
73
74 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
75 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
76 let range = max - min;
77
78 let mut sorted = values.to_vec();
80 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
81 let median = if count % 2 == 1 {
82 sorted[count / 2]
83 } else {
84 (sorted[count / 2 - 1] + sorted[count / 2]) / 2.0
85 };
86
87 let variance = if count > 1 {
88 values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (count as f64 - 1.0)
89 } else {
90 0.0
91 };
92 let std_dev = variance.sqrt();
93
94 Stats { count, min, max, range, mean, median, std_dev }
95}
96
97pub fn gaps(values: &[BrightDateValue]) -> Vec<f64> {
101 if values.len() < 2 {
102 return vec![];
103 }
104 let mut sorted = values.to_vec();
105 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
106 sorted.windows(2).map(|w| w[1] - w[0]).collect()
107}
108
109#[derive(Debug, Clone, PartialEq)]
111pub struct GapInfo {
112 pub gap: f64,
113 pub before: BrightDateValue,
114 pub after: BrightDateValue,
115}
116
117pub fn largest_gap(values: &[BrightDateValue]) -> Option<GapInfo> {
119 if values.len() < 2 {
120 return None;
121 }
122 let mut sorted = values.to_vec();
123 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
124
125 sorted
126 .windows(2)
127 .map(|w| GapInfo { gap: w[1] - w[0], before: w[0], after: w[1] })
128 .max_by(|a, b| a.gap.partial_cmp(&b.gap).unwrap_or(std::cmp::Ordering::Equal))
129}
130
131pub fn deduplicate(values: &[BrightDateValue], tolerance: f64) -> Vec<BrightDateValue> {
137 if values.is_empty() {
138 return vec![];
139 }
140 let mut sorted = values.to_vec();
141 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
142 let mut result = vec![sorted[0]];
143 for &v in &sorted[1..] {
144 let last = *result.last().unwrap();
145 if (v - last).abs() > tolerance {
146 result.push(v);
147 }
148 }
149 result
150}
151
152pub fn is_monotonically_increasing(values: &[BrightDateValue]) -> bool {
154 values.windows(2).all(|w| w[0] < w[1])
155}
156
157pub fn is_non_decreasing(values: &[BrightDateValue]) -> bool {
159 values.windows(2).all(|w| w[0] <= w[1])
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn closest_basic() {
168 assert_eq!(closest(5.0, &[1.0, 4.0, 7.0, 10.0]), Some(4.0));
169 assert_eq!(closest(5.0, &[]), None);
170 }
171
172 #[test]
173 fn statistics_basic() {
174 let s = statistics(&[1.0, 2.0, 3.0, 4.0, 5.0]);
175 assert_eq!(s.min, 1.0);
176 assert_eq!(s.max, 5.0);
177 assert_eq!(s.median, 3.0);
178 }
179
180 #[test]
181 #[should_panic(expected = "Cannot compute statistics of empty array")]
182 fn statistics_empty_panics() {
183 statistics(&[]);
184 }
185}