1fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
13 let y = if m <= 2 { y - 1 } else { y };
14 let era = if y >= 0 { y } else { y - 399 } / 400;
15 let yoe = y - era * 400;
16 let mp = if m > 2 { m - 3 } else { m + 9 };
17 let doy = (153 * mp + 2) / 5 + d - 1;
18 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
19 era * 146097 + doe - 719468
20}
21
22pub fn iso8601_to_ms(s: &str) -> Option<i64> {
25 let s = s.trim();
26 let b = s.as_bytes();
27 if b.len() < 10 || b[4] != b'-' || b[7] != b'-' {
28 return None;
29 }
30 let num = |r: std::ops::Range<usize>| -> Option<i64> { s.get(r)?.parse().ok() };
31 let (y, mo, d) = (num(0..4)?, num(5..7)?, num(8..10)?);
32 if !(1..=12).contains(&mo) || !(1..=31).contains(&d) {
33 return None;
34 }
35 let mut ms = days_from_civil(y, mo, d) * 86_400_000;
36 let mut i = 10;
37 if b.len() > i && (b[i] == b'T' || b[i] == b' ') {
38 i += 1;
39 if b.len() < i + 8 || b[i + 2] != b':' || b[i + 5] != b':' {
40 return None;
41 }
42 let (h, mi, sec) = (num(i..i + 2)?, num(i + 3..i + 5)?, num(i + 6..i + 8)?);
43 ms += (h * 3600 + mi * 60 + sec) * 1000;
44 i += 8;
45 if b.len() > i && b[i] == b'.' {
47 i += 1;
48 let start = i;
49 while i < b.len() && b[i].is_ascii_digit() {
50 i += 1;
51 }
52 let frac = &s[start..i];
53 if !frac.is_empty() {
54 let ms_str: String = frac.chars().chain("000".chars()).take(3).collect();
55 ms += ms_str.parse::<i64>().ok()?;
56 }
57 }
58 if b.len() > i {
60 match b[i] {
61 b'Z' | b'z' => {}
62 b'+' | b'-' => {
63 let sign: i64 = if b[i] == b'+' { 1 } else { -1 };
64 i += 1;
65 let oh = num(i..i + 2)?;
66 i += 2;
67 if b.len() > i && b[i] == b':' {
68 i += 1;
69 }
70 let om = if b.len() >= i + 2 { num(i..i + 2).unwrap_or(0) } else { 0 };
71 ms -= sign * (oh * 3600 + om * 60) * 1000;
72 }
73 _ => return None,
74 }
75 }
76 }
77 Some(ms)
78}
79
80pub fn now_ms() -> i64 {
88 std::time::SystemTime::now()
89 .duration_since(std::time::UNIX_EPOCH)
90 .map(|d| d.as_millis() as i64)
91 .unwrap_or(0)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn iso8601_parses_common_shapes() {
100 assert_eq!(iso8601_to_ms("1970-01-01T00:00:00Z"), Some(0));
101 assert_eq!(iso8601_to_ms("2024-01-01"), Some(1_704_067_200_000));
102 assert_eq!(iso8601_to_ms("2024-01-01T00:00:00Z"), Some(1_704_067_200_000));
103 assert_eq!(iso8601_to_ms("2024-01-01 00:00:00"), Some(1_704_067_200_000));
104 assert_eq!(iso8601_to_ms("2024-01-01T00:00:00.500Z"), Some(1_704_067_200_500));
105 assert_eq!(iso8601_to_ms("2023-12-31T17:00:00-07:00"), Some(1_704_067_200_000));
106 assert_eq!(iso8601_to_ms("2024-01-01T01:00:00+01:00"), Some(1_704_067_200_000));
107 assert_eq!(iso8601_to_ms("2024-01-01T00:00:00.982509Z"), Some(1_704_067_200_982));
108 assert_eq!(iso8601_to_ms("not a date"), None);
109 assert_eq!(iso8601_to_ms("2024-13-01"), None);
110 }
111
112 #[test]
114 fn leap_days_and_century_rules() {
115 let feb29 = iso8601_to_ms("2024-02-29T00:00:00Z").unwrap();
117 let mar01 = iso8601_to_ms("2024-03-01T00:00:00Z").unwrap();
118 assert_eq!(mar01 - feb29, 86_400_000);
119 assert_eq!(
121 iso8601_to_ms("2000-03-01T00:00:00Z").unwrap()
122 - iso8601_to_ms("2000-02-28T00:00:00Z").unwrap(),
123 2 * 86_400_000
124 );
125 }
126}