1use serde::{Deserialize, Serialize};
11
12#[derive(
19 Debug,
20 Clone,
21 Copy,
22 PartialEq,
23 Eq,
24 PartialOrd,
25 Ord,
26 Hash,
27 Serialize,
28 Deserialize,
29 zerompk::ToMessagePack,
30 zerompk::FromMessagePack,
31)]
32pub struct NdbDateTime {
33 pub micros: i64,
35}
36
37impl NdbDateTime {
38 pub fn from_micros(micros: i64) -> Self {
40 Self { micros }
41 }
42
43 pub fn from_millis(millis: i64) -> Self {
45 Self {
46 micros: millis * 1000,
47 }
48 }
49
50 pub fn from_secs(secs: i64) -> Self {
52 Self {
53 micros: secs * 1_000_000,
54 }
55 }
56
57 pub fn now() -> Self {
59 let dur = std::time::SystemTime::now()
60 .duration_since(std::time::UNIX_EPOCH)
61 .unwrap_or_default();
62 Self {
63 micros: dur.as_micros() as i64,
64 }
65 }
66
67 pub fn components(&self) -> DateTimeComponents {
69 let total_secs = self.micros / 1_000_000;
70 let micros_rem = (self.micros % 1_000_000).unsigned_abs();
71
72 let mut days = total_secs.div_euclid(86400) as i32;
74 let day_secs = total_secs.rem_euclid(86400) as u32;
75
76 days += 719_468; let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
78 let doe = (days - era * 146_097) as u32;
79 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
80 let y = yoe as i32 + era * 400;
81 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
82 let mp = (5 * doy + 2) / 153;
83 let d = doy - (153 * mp + 2) / 5 + 1;
84 let m = if mp < 10 { mp + 3 } else { mp - 9 };
85 let year = if m <= 2 { y + 1 } else { y };
86
87 DateTimeComponents {
88 year,
89 month: m as u8,
90 day: d as u8,
91 hour: (day_secs / 3600) as u8,
92 minute: ((day_secs % 3600) / 60) as u8,
93 second: (day_secs % 60) as u8,
94 microsecond: micros_rem as u32,
95 }
96 }
97
98 pub fn to_iso8601(&self) -> String {
100 let c = self.components();
101 format!(
102 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
103 c.year, c.month, c.day, c.hour, c.minute, c.second, c.microsecond
104 )
105 }
106
107 pub fn parse(s: &str) -> Option<Self> {
112 let s = s.trim().trim_end_matches('Z').trim_end_matches('z');
113
114 if s.len() == 10 {
115 let parts: Vec<&str> = s.split('-').collect();
117 if parts.len() != 3 {
118 return None;
119 }
120 let year: i32 = parts[0].parse().ok()?;
121 let month: u32 = parts[1].parse().ok()?;
122 let day: u32 = parts[2].parse().ok()?;
123 return Some(Self::from_civil(year, month, day, 0, 0, 0, 0));
124 }
125
126 let (date_part, time_part) = s.split_once('T').or_else(|| s.split_once(' '))?;
128 let date_parts: Vec<&str> = date_part.split('-').collect();
129 if date_parts.len() != 3 {
130 return None;
131 }
132 let year: i32 = date_parts[0].parse().ok()?;
133 let month: u32 = date_parts[1].parse().ok()?;
134 let day: u32 = date_parts[2].parse().ok()?;
135
136 let (time_main, frac) = if let Some((t, f)) = time_part.split_once('.') {
137 (t, f)
138 } else {
139 (time_part, "0")
140 };
141 let time_parts: Vec<&str> = time_main.split(':').collect();
142 if time_parts.len() < 2 {
143 return None;
144 }
145 let hour: u32 = time_parts[0].parse().ok()?;
146 let minute: u32 = time_parts[1].parse().ok()?;
147 let second: u32 = time_parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
148
149 let frac_padded = format!("{frac:0<6}");
151 let micros: u32 = frac_padded[..6].parse().unwrap_or(0);
152
153 Some(Self::from_civil(
154 year, month, day, hour, minute, second, micros,
155 ))
156 }
157
158 fn from_civil(
160 year: i32,
161 month: u32,
162 day: u32,
163 hour: u32,
164 minute: u32,
165 second: u32,
166 micros: u32,
167 ) -> Self {
168 let y = if month <= 2 { year - 1 } else { year };
170 let m = if month <= 2 { month + 9 } else { month - 3 };
171 let era = if y >= 0 { y } else { y - 399 } / 400;
172 let yoe = (y - era * 400) as u32;
173 let doy = (153 * m + 2) / 5 + day - 1;
174 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
175 let days = era as i64 * 146_097 + doe as i64 - 719_468;
176 let total_secs = days * 86400 + hour as i64 * 3600 + minute as i64 * 60 + second as i64;
177 Self {
178 micros: total_secs * 1_000_000 + micros as i64,
179 }
180 }
181
182 pub fn add_duration(&self, d: NdbDuration) -> Self {
184 Self {
185 micros: self.micros + d.micros,
186 }
187 }
188
189 pub fn sub_duration(&self, d: NdbDuration) -> Self {
191 Self {
192 micros: self.micros - d.micros,
193 }
194 }
195
196 pub fn duration_since(&self, other: &NdbDateTime) -> NdbDuration {
198 NdbDuration {
199 micros: self.micros - other.micros,
200 }
201 }
202
203 pub fn unix_secs(&self) -> i64 {
205 self.micros / 1_000_000
206 }
207
208 pub fn unix_millis(&self) -> i64 {
210 self.micros / 1_000
211 }
212}
213
214impl std::fmt::Display for NdbDateTime {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 f.write_str(&self.to_iso8601())
217 }
218}
219
220#[derive(Debug, Clone, Copy)]
222pub struct DateTimeComponents {
223 pub year: i32,
224 pub month: u8,
225 pub day: u8,
226 pub hour: u8,
227 pub minute: u8,
228 pub second: u8,
229 pub microsecond: u32,
230}
231
232#[derive(
236 Debug,
237 Clone,
238 Copy,
239 PartialEq,
240 Eq,
241 PartialOrd,
242 Ord,
243 Hash,
244 Serialize,
245 Deserialize,
246 zerompk::ToMessagePack,
247 zerompk::FromMessagePack,
248)]
249pub struct NdbDuration {
250 pub micros: i64,
252}
253
254impl NdbDuration {
255 pub fn from_micros(micros: i64) -> Self {
256 Self { micros }
257 }
258
259 pub fn from_millis(millis: i64) -> Self {
260 Self {
261 micros: millis * 1_000,
262 }
263 }
264
265 pub fn from_secs(secs: i64) -> Self {
266 Self {
267 micros: secs * 1_000_000,
268 }
269 }
270
271 pub fn from_minutes(mins: i64) -> Self {
272 Self {
273 micros: mins * 60 * 1_000_000,
274 }
275 }
276
277 pub fn from_hours(hours: i64) -> Self {
278 Self {
279 micros: hours * 3600 * 1_000_000,
280 }
281 }
282
283 pub fn from_days(days: i64) -> Self {
284 Self {
285 micros: days * 86400 * 1_000_000,
286 }
287 }
288
289 pub fn as_secs_f64(&self) -> f64 {
290 self.micros as f64 / 1_000_000.0
291 }
292
293 pub fn as_millis(&self) -> i64 {
294 self.micros / 1_000
295 }
296
297 pub fn to_human(&self) -> String {
299 let abs = self.micros.unsigned_abs();
300 let sign = if self.micros < 0 { "-" } else { "" };
301
302 if abs < 1_000 {
303 return format!("{sign}{abs}us");
304 }
305 if abs < 1_000_000 {
306 return format!("{sign}{}ms", abs / 1_000);
307 }
308
309 let total_secs = abs / 1_000_000;
310 let hours = total_secs / 3600;
311 let mins = (total_secs % 3600) / 60;
312 let secs = total_secs % 60;
313
314 if hours > 0 {
315 if mins > 0 || secs > 0 {
316 format!("{sign}{hours}h{mins}m{secs}s")
317 } else {
318 format!("{sign}{hours}h")
319 }
320 } else if mins > 0 {
321 if secs > 0 {
322 format!("{sign}{mins}m{secs}s")
323 } else {
324 format!("{sign}{mins}m")
325 }
326 } else {
327 format!("{sign}{secs}s")
328 }
329 }
330
331 pub fn parse(s: &str) -> Option<Self> {
333 let s = s.trim();
334 if s.is_empty() {
335 return None;
336 }
337
338 let (neg, s) = if let Some(rest) = s.strip_prefix('-') {
339 (true, rest)
340 } else {
341 (false, s)
342 };
343
344 if let Some(n) = s.strip_suffix("us") {
346 let v: i64 = n.trim().parse().ok()?;
347 return Some(Self::from_micros(if neg { -v } else { v }));
348 }
349 if let Some(n) = s.strip_suffix("ms") {
350 let v: i64 = n.trim().parse().ok()?;
351 return Some(Self::from_millis(if neg { -v } else { v }));
352 }
353 if let Some(n) = s.strip_suffix('d') {
354 let v: i64 = n.trim().parse().ok()?;
355 return Some(Self::from_days(if neg { -v } else { v }));
356 }
357
358 let mut total_micros: i64 = 0;
360 let mut num_buf = String::new();
361 for c in s.chars() {
362 if c.is_ascii_digit() {
363 num_buf.push(c);
364 } else {
365 let n: i64 = num_buf.parse().ok()?;
366 num_buf.clear();
367 match c {
368 'h' => total_micros += n * 3_600_000_000,
369 'm' => total_micros += n * 60_000_000,
370 's' => total_micros += n * 1_000_000,
371 _ => return None,
372 }
373 }
374 }
375 if !num_buf.is_empty() {
377 let n: i64 = num_buf.parse().ok()?;
378 total_micros += n * 1_000_000;
379 }
380
381 if total_micros == 0 {
382 return None;
383 }
384
385 Some(Self::from_micros(if neg {
386 -total_micros
387 } else {
388 total_micros
389 }))
390 }
391}
392
393impl std::fmt::Display for NdbDuration {
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 f.write_str(&self.to_human())
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn datetime_now_roundtrip() {
405 let dt = NdbDateTime::now();
406 let iso = dt.to_iso8601();
407 let parsed = NdbDateTime::parse(&iso).unwrap();
408 assert!(
410 (dt.micros - parsed.micros).abs() <= 1,
411 "dt={}, parsed={}",
412 dt.micros,
413 parsed.micros
414 );
415 }
416
417 #[test]
418 fn datetime_epoch() {
419 let dt = NdbDateTime::from_micros(0);
420 assert_eq!(dt.to_iso8601(), "1970-01-01T00:00:00.000000Z");
421 }
422
423 #[test]
424 fn datetime_known_date() {
425 let dt = NdbDateTime::parse("2024-03-15T10:30:00Z").unwrap();
426 let c = dt.components();
427 assert_eq!(c.year, 2024);
428 assert_eq!(c.month, 3);
429 assert_eq!(c.day, 15);
430 assert_eq!(c.hour, 10);
431 assert_eq!(c.minute, 30);
432 assert_eq!(c.second, 0);
433 }
434
435 #[test]
436 fn datetime_fractional_seconds() {
437 let dt = NdbDateTime::parse("2024-01-01T00:00:00.123456Z").unwrap();
438 let c = dt.components();
439 assert_eq!(c.microsecond, 123456);
440 }
441
442 #[test]
443 fn datetime_date_only() {
444 let dt = NdbDateTime::parse("2024-03-15").unwrap();
445 let c = dt.components();
446 assert_eq!(c.year, 2024);
447 assert_eq!(c.month, 3);
448 assert_eq!(c.day, 15);
449 assert_eq!(c.hour, 0);
450 }
451
452 #[test]
453 fn datetime_arithmetic() {
454 let dt = NdbDateTime::parse("2024-01-01T00:00:00Z").unwrap();
455 let later = dt.add_duration(NdbDuration::from_hours(24));
456 let c = later.components();
457 assert_eq!(c.day, 2);
458 }
459
460 #[test]
461 fn datetime_ordering() {
462 let a = NdbDateTime::parse("2024-01-01T00:00:00Z").unwrap();
463 let b = NdbDateTime::parse("2024-01-02T00:00:00Z").unwrap();
464 assert!(a < b);
465 }
466
467 #[test]
468 fn duration_human_format() {
469 assert_eq!(NdbDuration::from_secs(90).to_human(), "1m30s");
470 assert_eq!(NdbDuration::from_hours(2).to_human(), "2h");
471 assert_eq!(NdbDuration::from_millis(500).to_human(), "500ms");
472 assert_eq!(NdbDuration::from_micros(42).to_human(), "42us");
473 assert_eq!(NdbDuration::from_secs(3661).to_human(), "1h1m1s");
474 }
475
476 #[test]
477 fn duration_parse() {
478 assert_eq!(NdbDuration::parse("30s").unwrap().micros, 30_000_000);
479 assert_eq!(NdbDuration::parse("1h30m").unwrap().micros, 5_400_000_000);
480 assert_eq!(NdbDuration::parse("500ms").unwrap().micros, 500_000);
481 assert_eq!(NdbDuration::parse("2d").unwrap().micros, 172_800_000_000);
482 assert_eq!(NdbDuration::parse("-5s").unwrap().micros, -5_000_000);
483 }
484
485 #[test]
486 fn duration_roundtrip() {
487 let d = NdbDuration::from_secs(3661);
488 let s = d.to_human();
489 let parsed = NdbDuration::parse(&s).unwrap();
490 assert_eq!(d.micros, parsed.micros);
491 }
492
493 #[test]
494 fn unix_accessors() {
495 let dt = NdbDateTime::from_secs(1_700_000_000);
496 assert_eq!(dt.unix_secs(), 1_700_000_000);
497 assert_eq!(dt.unix_millis(), 1_700_000_000_000);
498 }
499}