1use std::time::{Duration, Instant};
6
7use time::format_description::well_known::Rfc3339;
8use time::OffsetDateTime;
9
10use crate::{SysprimsError, SysprimsResult};
11
12const MIN_TICK_INTERVAL: Duration = Duration::from_millis(1);
14
15pub fn now_rfc3339() -> String {
19 OffsetDateTime::now_utc()
20 .format(&Rfc3339)
21 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
22}
23
24pub struct Tick {
46 interval: Duration,
47 next: Instant,
48}
49
50impl Tick {
51 pub fn new(interval: Duration) -> SysprimsResult<Self> {
55 if interval < MIN_TICK_INTERVAL {
56 return Err(SysprimsError::invalid_argument(format!(
57 "tick interval must be >= 1ms, got {:?}",
58 interval
59 )));
60 }
61 Ok(Self {
62 interval,
63 next: Instant::now() + interval,
64 })
65 }
66
67 pub fn sleep_until_next(&mut self) {
73 let now = Instant::now();
74 if self.next > now {
75 std::thread::sleep(self.next - now);
76 }
77 let now = Instant::now();
79 if self.next <= now {
80 let behind = now - self.next;
81 let skipped = behind.as_nanos() / self.interval.as_nanos() + 1;
82 self.next += self.interval * skipped as u32;
83 } else {
84 self.next += self.interval;
85 }
86 }
87
88 pub fn interval(&self) -> Duration {
90 self.interval
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_now_rfc3339_format() {
100 let ts = now_rfc3339();
101 assert!(ts.contains('T'), "timestamp should contain T: {}", ts);
103 assert!(ts.ends_with('Z'), "timestamp should end with Z: {}", ts);
104 assert!(
106 ts.len() >= 20,
107 "timestamp should be at least 20 chars: {}",
108 ts
109 );
110 assert_eq!(&ts[4..5], "-", "should have dash at pos 4: {}", ts);
111 assert_eq!(&ts[7..8], "-", "should have dash at pos 7: {}", ts);
112 }
113
114 #[test]
115 fn test_tick_rejects_zero_interval() {
116 let result = Tick::new(Duration::ZERO);
117 assert!(result.is_err(), "zero interval should be rejected");
118 }
119
120 #[test]
121 fn test_tick_rejects_sub_millisecond() {
122 let result = Tick::new(Duration::from_micros(500));
123 assert!(result.is_err(), "sub-ms interval should be rejected");
124 }
125
126 #[test]
127 fn test_tick_accepts_one_millisecond() {
128 let result = Tick::new(Duration::from_millis(1));
129 assert!(result.is_ok(), "1ms interval should be accepted");
130 }
131
132 #[test]
133 fn test_tick_sleeps_for_interval() {
134 let mut tick = Tick::new(Duration::from_millis(50)).unwrap();
135 let start = Instant::now();
136 tick.sleep_until_next();
137 let elapsed = start.elapsed();
138 assert!(
139 elapsed >= Duration::from_millis(40),
140 "should sleep roughly 50ms, got {:?}",
141 elapsed
142 );
143 assert!(
144 elapsed < Duration::from_millis(200),
145 "should not overshoot by too much: {:?}",
146 elapsed
147 );
148 }
149
150 #[test]
151 fn test_tick_interval_accessor() {
152 let tick = Tick::new(Duration::from_secs(10)).unwrap();
153 assert_eq!(tick.interval(), Duration::from_secs(10));
154 }
155}