nodedb_types/datetime/
duration.rs1use serde::{Deserialize, Serialize};
6
7use super::error::NdbDateTimeError;
8
9#[non_exhaustive]
16#[derive(
17 Debug,
18 Clone,
19 Copy,
20 PartialEq,
21 Eq,
22 PartialOrd,
23 Ord,
24 Hash,
25 Serialize,
26 Deserialize,
27 zerompk::ToMessagePack,
28 zerompk::FromMessagePack,
29)]
30pub struct NdbDuration {
31 pub micros: i64,
33}
34
35impl NdbDuration {
36 pub fn from_micros(micros: i64) -> Self {
37 Self { micros }
38 }
39
40 pub fn from_millis(millis: i64) -> Result<Self, NdbDateTimeError> {
44 let micros = millis
45 .checked_mul(1_000)
46 .ok_or(NdbDateTimeError::Overflow {
47 input: millis,
48 unit: "millis",
49 })?;
50 Ok(Self { micros })
51 }
52
53 pub fn from_secs(secs: i64) -> Result<Self, NdbDateTimeError> {
57 let micros = secs
58 .checked_mul(1_000_000)
59 .ok_or(NdbDateTimeError::Overflow {
60 input: secs,
61 unit: "secs",
62 })?;
63 Ok(Self { micros })
64 }
65
66 pub fn from_minutes(mins: i64) -> Result<Self, NdbDateTimeError> {
70 let micros = mins
71 .checked_mul(60_000_000)
72 .ok_or(NdbDateTimeError::Overflow {
73 input: mins,
74 unit: "minutes",
75 })?;
76 Ok(Self { micros })
77 }
78
79 pub fn from_hours(hours: i64) -> Result<Self, NdbDateTimeError> {
83 let micros = hours
84 .checked_mul(3_600_000_000)
85 .ok_or(NdbDateTimeError::Overflow {
86 input: hours,
87 unit: "hours",
88 })?;
89 Ok(Self { micros })
90 }
91
92 pub fn from_days(days: i64) -> Result<Self, NdbDateTimeError> {
96 let micros = days
97 .checked_mul(86_400_000_000)
98 .ok_or(NdbDateTimeError::Overflow {
99 input: days,
100 unit: "days",
101 })?;
102 Ok(Self { micros })
103 }
104
105 pub fn as_secs_f64(&self) -> f64 {
106 self.micros as f64 / 1_000_000.0
107 }
108
109 pub fn as_millis(&self) -> i64 {
110 self.micros / 1_000
111 }
112
113 pub fn to_human(&self) -> String {
115 let abs = self.micros.unsigned_abs();
116 let sign = if self.micros < 0 { "-" } else { "" };
117
118 if abs < 1_000 {
119 return format!("{sign}{abs}us");
120 }
121 if abs < 1_000_000 {
122 return format!("{sign}{}ms", abs / 1_000);
123 }
124
125 let total_secs = abs / 1_000_000;
126 let hours = total_secs / 3600;
127 let mins = (total_secs % 3600) / 60;
128 let secs = total_secs % 60;
129
130 if hours > 0 {
131 if mins > 0 || secs > 0 {
132 format!("{sign}{hours}h{mins}m{secs}s")
133 } else {
134 format!("{sign}{hours}h")
135 }
136 } else if mins > 0 {
137 if secs > 0 {
138 format!("{sign}{mins}m{secs}s")
139 } else {
140 format!("{sign}{mins}m")
141 }
142 } else {
143 format!("{sign}{secs}s")
144 }
145 }
146
147 pub fn parse(s: &str) -> Option<Self> {
151 let s = s.trim();
152 if s.is_empty() {
153 return None;
154 }
155
156 let (neg, s) = if let Some(rest) = s.strip_prefix('-') {
157 (true, rest)
158 } else {
159 (false, s)
160 };
161
162 if let Some(n) = s.strip_suffix("us") {
164 let v: i64 = n.trim().parse().ok()?;
165 return Some(Self::from_micros(if neg { -v } else { v }));
166 }
167 if let Some(n) = s.strip_suffix("ms") {
168 let v: i64 = n.trim().parse().ok()?;
169 let d = Self::from_millis(if neg { -v } else { v }).ok()?;
170 return Some(d);
171 }
172 if let Some(n) = s.strip_suffix('d') {
173 let v: i64 = n.trim().parse().ok()?;
174 let d = Self::from_days(if neg { -v } else { v }).ok()?;
175 return Some(d);
176 }
177
178 let mut total_micros: i64 = 0;
180 let mut num_buf = String::new();
181 for c in s.chars() {
182 if c.is_ascii_digit() {
183 num_buf.push(c);
184 } else {
185 let n: i64 = num_buf.parse().ok()?;
186 num_buf.clear();
187 let part = match c {
188 'h' => n.checked_mul(3_600_000_000)?,
189 'm' => n.checked_mul(60_000_000)?,
190 's' => n.checked_mul(1_000_000)?,
191 _ => return None,
192 };
193 total_micros = total_micros.checked_add(part)?;
194 }
195 }
196 if !num_buf.is_empty() {
198 let n: i64 = num_buf.parse().ok()?;
199 let part = n.checked_mul(1_000_000)?;
200 total_micros = total_micros.checked_add(part)?;
201 }
202
203 if total_micros == 0 {
204 return None;
205 }
206
207 Some(Self::from_micros(if neg {
208 -total_micros
209 } else {
210 total_micros
211 }))
212 }
213}
214
215impl std::fmt::Display for NdbDuration {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.write_str(&self.to_human())
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn duration_human_format() {
227 assert_eq!(
228 NdbDuration::from_secs(90).expect("90s in range").to_human(),
229 "1m30s"
230 );
231 assert_eq!(
232 NdbDuration::from_hours(2).expect("2h in range").to_human(),
233 "2h"
234 );
235 assert_eq!(
236 NdbDuration::from_millis(500)
237 .expect("500ms in range")
238 .to_human(),
239 "500ms"
240 );
241 assert_eq!(NdbDuration::from_micros(42).to_human(), "42us");
242 assert_eq!(
243 NdbDuration::from_secs(3661)
244 .expect("3661s in range")
245 .to_human(),
246 "1h1m1s"
247 );
248 }
249
250 #[test]
251 fn duration_parse() {
252 assert_eq!(NdbDuration::parse("30s").unwrap().micros, 30_000_000);
253 assert_eq!(NdbDuration::parse("1h30m").unwrap().micros, 5_400_000_000);
254 assert_eq!(NdbDuration::parse("500ms").unwrap().micros, 500_000);
255 assert_eq!(NdbDuration::parse("2d").unwrap().micros, 172_800_000_000);
256 assert_eq!(NdbDuration::parse("-5s").unwrap().micros, -5_000_000);
257 }
258
259 #[test]
260 fn duration_roundtrip() {
261 let d = NdbDuration::from_secs(3661).expect("3661s in range");
262 let s = d.to_human();
263 let parsed = NdbDuration::parse(&s).unwrap();
264 assert_eq!(d.micros, parsed.micros);
265 }
266
267 #[test]
268 fn duration_from_millis_overflow() {
269 assert!(NdbDuration::from_millis(i64::MAX).is_err());
270 }
271
272 #[test]
273 fn duration_from_secs_overflow() {
274 assert!(NdbDuration::from_secs(i64::MAX).is_err());
275 }
276
277 #[test]
278 fn duration_from_minutes_overflow() {
279 assert!(NdbDuration::from_minutes(i64::MAX).is_err());
280 }
281
282 #[test]
283 fn duration_from_hours_overflow() {
284 assert!(NdbDuration::from_hours(i64::MAX).is_err());
285 }
286
287 #[test]
288 fn duration_from_days_overflow() {
289 assert!(NdbDuration::from_days(i64::MAX).is_err());
290 }
291
292 #[test]
293 fn duration_parse_overflow() {
294 let overflow_str = "9999999999999999h";
296 assert!(NdbDuration::parse(overflow_str).is_none());
297 }
298}