1use core::time::Duration;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct DutyCycle {
26 active: Duration,
27 sleep: Duration,
28}
29
30impl DutyCycle {
31 pub fn new(active: Duration, sleep: Duration) -> Self {
42 Self { active, sleep }
43 }
44
45 pub fn from_fraction(period: Duration, fraction: f32) -> Self {
58 let active = if fraction >= 1.0 {
59 period
60 } else if fraction > 0.0 {
61 period.mul_f64(f64::from(fraction))
62 } else {
63 Duration::ZERO
64 };
65 Self {
66 active,
67 sleep: period - active,
68 }
69 }
70
71 pub fn active(&self) -> Duration {
73 self.active
74 }
75
76 pub fn sleep(&self) -> Duration {
78 self.sleep
79 }
80
81 pub fn period(&self) -> Duration {
84 self.active.saturating_add(self.sleep)
85 }
86
87 pub fn fraction(&self) -> f32 {
93 let period = self.period();
94 if period.is_zero() {
95 0.0
96 } else {
97 self.active.as_secs_f32() / period.as_secs_f32()
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn period_is_awake_plus_asleep() {
108 let cycle = DutyCycle::new(Duration::from_secs(2), Duration::from_secs(8));
109 assert_eq!(cycle.period(), Duration::from_secs(10));
110 }
111
112 #[test]
113 fn fraction_is_the_awake_share() {
114 let cycle = DutyCycle::new(Duration::from_secs(2), Duration::from_secs(8));
115 assert!((cycle.fraction() - 0.2).abs() < 1e-6);
116 }
117
118 #[test]
119 fn a_zero_period_has_zero_fraction() {
120 let cycle = DutyCycle::new(Duration::ZERO, Duration::ZERO);
121 assert_eq!(cycle.fraction(), 0.0);
122 }
123
124 #[test]
125 fn from_fraction_splits_the_period() {
126 let cycle = DutyCycle::from_fraction(Duration::from_secs(10), 0.25);
127 assert!((cycle.active().as_secs_f32() - 2.5).abs() < 1e-3);
128 assert!((cycle.fraction() - 0.25).abs() < 1e-6);
129 }
130
131 #[test]
132 fn from_fraction_clamps_out_of_range_values() {
133 let all_awake = DutyCycle::from_fraction(Duration::from_secs(10), 5.0);
134 assert_eq!(all_awake.active(), Duration::from_secs(10));
135 assert_eq!(all_awake.sleep(), Duration::ZERO);
136
137 let all_asleep = DutyCycle::from_fraction(Duration::from_secs(10), -1.0);
138 assert_eq!(all_asleep.active(), Duration::ZERO);
139 }
140
141 #[test]
142 fn a_fraction_that_is_not_a_number_keeps_the_node_asleep() {
143 let cycle = DutyCycle::from_fraction(Duration::from_secs(10), f32::NAN);
144 assert_eq!(cycle.active(), Duration::ZERO);
145 assert_eq!(cycle.sleep(), Duration::from_secs(10));
146 }
147
148 #[test]
149 fn the_longest_period_splits_without_overflowing() {
150 let whole = DutyCycle::from_fraction(Duration::MAX, 1.0);
151 assert_eq!(whole.active(), Duration::MAX);
152 let nearly = DutyCycle::from_fraction(Duration::MAX, 1.0 - f32::EPSILON);
153 assert!(nearly.active() < Duration::MAX);
154 }
155
156 #[test]
157 fn the_longest_halves_report_their_period_without_overflowing() {
158 let cycle = DutyCycle::new(Duration::MAX, Duration::from_secs(1));
159 assert_eq!(cycle.period(), Duration::MAX);
160 assert!((cycle.fraction() - 1.0).abs() < 1e-6);
161 }
162}