1use std::{
2 f64::consts::TAU,
3 fmt::{Debug, Display},
4};
5
6use num_traits::Float;
7
8#[cfg(feature = "chrono")]
9mod chrono;
10#[cfg(feature = "chrono")]
11pub use crate::chrono::avg_time_of_day;
12
13pub fn circadian_average<I, F>(range: F, data: I) -> (F, F)
14where
15 F: Float + Debug + Display,
16 I: Iterator<Item = F>,
17{
18 let mut len = 0;
19 let mut x_pos_sum = F::zero();
20 let mut y_pos_sum = F::zero();
21
22 let tau = F::from(TAU).unwrap();
24 let tau_over_range = tau / range;
25
26 for x in data {
27 debug_assert!(x >= F::zero(), "Input data must be positive");
28 len += 1;
29 let angle = x * tau_over_range;
31 let (s, c) = angle.sin_cos();
32 x_pos_sum = x_pos_sum + c;
33 y_pos_sum = y_pos_sum + s;
34 }
35
36 let avg_x_pos = x_pos_sum / F::from(len).unwrap();
37 let avg_y_pos = y_pos_sum / F::from(len).unwrap();
38 let mut avg_angle = avg_y_pos.atan2(avg_x_pos);
40 if avg_angle < F::zero() {
41 avg_angle = avg_angle + tau;
42 }
43 let avg_value = avg_angle / tau_over_range;
45 let confidence = (avg_x_pos.powi(2) + avg_y_pos.powi(2)).sqrt();
47 (avg_value, confidence)
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use float_cmp::approx_eq;
54 use std::f64::consts::FRAC_1_SQRT_2;
55
56 #[test]
57 #[should_panic(expected = "Input data must be positive")]
58 fn negative_input_fails() {
59 let data = vec![-1.0];
60 let _ = circadian_average(4.0, data.into_iter());
61 }
62
63 #[test]
64 fn test_circadian_average_unanimous() {
65 let data = vec![0.0, 0.0];
66 let (avg, confidence) = circadian_average(4.0, data.into_iter());
67 assert_eq!(avg, 0.0);
68 assert_eq!(confidence, 1.0);
69 }
70
71 #[test]
72 fn test_circadian_crossing_zero() {
73 let data = vec![0.5, 3.5];
74 let (avg, confidence) = circadian_average(4.0, data.into_iter());
75 assert!(approx_eq!(f64, avg, 4.0, epsilon = 0.0001));
76 assert!(approx_eq!(f64, confidence, FRAC_1_SQRT_2, epsilon = 0.0001));
77 }
78
79 #[test]
80 fn test_circadian_average_split() {
81 let data = vec![1.0, 2.0];
82 let (avg, confidence) = circadian_average(4.0, data.into_iter());
83 assert_eq!(avg, 1.5);
84 assert!(approx_eq!(f64, confidence, FRAC_1_SQRT_2, epsilon = 0.0001));
85 }
86
87 #[test]
88 fn test_circadian_average_even_split() {
89 let data = vec![0.0, 2.0];
90 let (avg, confidence) = circadian_average(4.0, data.into_iter());
91 assert!(approx_eq!(f64, avg, 1.0, epsilon = 0.0001));
92 assert!(approx_eq!(f64, confidence, 0.0, epsilon = 0.0001));
93 }
94
95 #[test]
96 fn test_average_in_lower_left_quadrant() {
97 let data = vec![2.0, 3.0];
98 let (avg, confidence) = circadian_average(4.0, data.into_iter());
99 assert!(approx_eq!(f64, avg, 2.5, epsilon = 0.0001));
100 assert!(approx_eq!(f64, confidence, FRAC_1_SQRT_2, epsilon = 0.0001));
101 }
102
103 #[test]
104 fn avg_count() {
105 const FACTOR: f64 = 607.0;
106 let data: Vec<f64> = vec![
107 514.0, 176.0, 64.0, 249.0, 415.0, 455.0, 221.0, 375.0, 477.0, 464.0, 421.0, 32.0, 40.0,
108 496.0, 534.0, 134.0,
109 ];
110 let inputs = data.into_iter().map(|x| x);
111 let (avg, confidence) = circadian_average(FACTOR, inputs);
112 assert_eq!(avg, 498.7531532014195);
113 assert_eq!(confidence, 0.23138448716890458)
114 }
115}