circadian_tools/
lib.rs

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    // This unwrap is reasonable as this can not be done if F can't be represented as 2PI
23    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        // Get X, Y position of each data point on a circle with a perimeter of range
30        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    // Get the angle of the average position
39    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    // Convert the angle to a value on the range
44    let avg_value = avg_angle / tau_over_range;
45    // Get the confidence, which is the distance of the average from the origin
46    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}