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