1use std::time::{SystemTime, UNIX_EPOCH};
7
8pub fn now_millis() -> i64 {
11 SystemTime::now()
12 .duration_since(UNIX_EPOCH)
13 .map(|elapsed| elapsed.as_millis() as i64)
14 .unwrap_or(0)
15}
16
17pub fn iso8601(millis: i64) -> String {
20 let seconds = millis.div_euclid(1000);
21 let days = seconds.div_euclid(86_400);
22 let seconds_of_day = seconds.rem_euclid(86_400);
23 let hour = seconds_of_day / 3_600;
24 let minute = (seconds_of_day % 3_600) / 60;
25 let second = seconds_of_day % 60;
26 let (year, month, day) = civil_from_days(days);
27 format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
28}
29
30pub fn millis_from_iso8601(text: &str) -> Option<i64> {
37 let text = text.trim().trim_end_matches('Z');
38 let (date, time) = text.split_once('T')?;
39 let (time, offset_seconds) = match time.rfind(['+', '-']) {
40 Some(at) => {
41 let (time, offset) = time.split_at(at);
42 let (hours, minutes) = offset[1..].split_once(':')?;
43 let seconds = hours.parse::<i64>().ok()? * 3_600 + minutes.parse::<i64>().ok()? * 60;
44 (
45 time,
46 if offset.starts_with('-') {
47 -seconds
48 } else {
49 seconds
50 },
51 )
52 }
53 None => (time, 0),
54 };
55 let mut date_parts = date.split('-');
56 let year: i64 = date_parts.next()?.parse().ok()?;
57 let month: u32 = date_parts.next()?.parse().ok()?;
58 let day: u32 = date_parts.next()?.parse().ok()?;
59 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
60 return None;
61 }
62 let (hms, fraction) = match time.split_once('.') {
63 Some((hms, fraction)) => (hms, Some(fraction)),
64 None => (time, None),
65 };
66 let mut time_parts = hms.split(':');
67 let hour: i64 = time_parts.next()?.parse().ok()?;
68 let minute: i64 = time_parts.next()?.parse().ok()?;
69 let second: i64 = time_parts.next()?.parse().ok()?;
70 let sub_millis: i64 = fraction
71 .map(|fraction| {
72 let digits: String = fraction
73 .chars()
74 .take_while(char::is_ascii_digit)
75 .take(3)
76 .collect();
77 format!("{digits:0<3}").parse().unwrap_or(0)
78 })
79 .unwrap_or(0);
80 let days = days_from_civil(year, month, day);
81 Some(
82 (days * 86_400 + hour * 3_600 + minute * 60 + second - offset_seconds) * 1_000 + sub_millis,
83 )
84}
85
86pub(crate) fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
90 let year = if month <= 2 { year - 1 } else { year };
91 let era = if year >= 0 { year } else { year - 399 } / 400;
92 let year_of_era = year - era * 400; let month = i64::from(month);
94 let day_of_year =
95 (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + i64::from(day) - 1; let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year; era * 146_097 + day_of_era - 719_468
98}
99
100pub(crate) fn civil_from_days(days: i64) -> (i64, u32, u32) {
103 let z = days + 719_468;
105 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
106 let day_of_era = (z - era * 146_097) as u64; let year_of_era =
108 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365; let year = year_of_era as i64 + era * 400;
110 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); let month_position = (5 * day_of_year + 2) / 153; let day = (day_of_year - (153 * month_position + 2) / 5 + 1) as u32; let month = if month_position < 10 {
114 month_position + 3
115 } else {
116 month_position - 9
117 } as u32; let year = if month <= 2 { year + 1 } else { year };
119 (year, month, day)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn the_epoch_is_formatted() {
128 assert_eq!(iso8601(0), "1970-01-01T00:00:00Z");
129 }
130
131 #[test]
132 fn a_known_instant_is_formatted() {
133 assert_eq!(iso8601(1_600_000_000_000), "2020-09-13T12:26:40Z");
134 }
135
136 #[test]
137 fn sub_second_millis_truncate_to_the_second() {
138 assert_eq!(iso8601(1_600_000_000_999), "2020-09-13T12:26:40Z");
139 }
140
141 #[test]
142 fn a_leap_day_is_handled() {
143 assert_eq!(iso8601(1_582_934_400_000), "2020-02-29T00:00:00Z");
145 }
146
147 #[test]
148 fn a_pre_epoch_instant_formats_in_the_past() {
149 assert_eq!(iso8601(-1_000), "1969-12-31T23:59:59Z");
150 }
151
152 #[test]
153 fn iso8601_round_trips_through_millis() {
154 for millis in [0, 1_600_000_000_000, 1_582_934_400_000] {
155 let text = iso8601(millis);
156 assert_eq!(millis_from_iso8601(&text), Some(millis));
157 }
158 }
159
160 #[test]
161 fn civil_dates_anchor_at_the_epoch() {
162 assert_eq!(days_from_civil(1970, 1, 1), 0);
163 assert_eq!(days_from_civil(1970, 1, 2), 1);
164 assert_eq!(days_from_civil(2024, 1, 15), 19737);
165 assert_eq!(days_from_civil(2000, 3, 1), 11017);
167 assert_eq!(days_from_civil(2024, 2, 29), 19782);
168 }
169
170 #[test]
171 fn the_parser_handles_fractions_and_never_panics() {
172 assert_eq!(millis_from_iso8601("1970-01-01T00:00:00Z"), Some(0));
173 assert_eq!(
174 millis_from_iso8601("2024-01-15T10:30:00.000Z"),
175 Some(1_705_314_600_000)
176 );
177 assert_eq!(
178 millis_from_iso8601("2024-01-15T10:30:00Z"),
179 Some(1_705_314_600_000)
180 );
181 assert_eq!(
183 millis_from_iso8601("2024-01-15T10:30:00.5Z"),
184 Some(1_705_314_600_500)
185 );
186 assert_eq!(
187 millis_from_iso8601("2024-01-15T10:30:00.12Z"),
188 Some(1_705_314_600_120)
189 );
190 assert_eq!(
191 millis_from_iso8601("2024-01-15T10:30:00.123456Z"),
192 Some(1_705_314_600_123)
193 );
194 assert_eq!(
196 millis_from_iso8601("2024-01-15T10:30:00.12éZ"),
197 Some(1_705_314_600_120)
198 );
199 }
200
201 #[test]
202 fn a_malformed_timestamp_does_not_parse() {
203 assert_eq!(millis_from_iso8601("not-a-time"), None);
204 assert_eq!(millis_from_iso8601("2020/09/13 12:26:40"), None);
205 assert_eq!(millis_from_iso8601("2024-01-15T10:30:00+02"), None);
206 assert_eq!(millis_from_iso8601("2024-13-01T00:00:00Z"), None);
207 assert_eq!(millis_from_iso8601(""), None);
208 }
209
210 #[test]
211 fn offsets_shift_to_utc() {
212 assert_eq!(
213 millis_from_iso8601("2023-11-14T22:13:20Z"),
214 Some(1_700_000_000_000)
215 );
216 assert_eq!(
217 millis_from_iso8601("2023-11-15T01:13:20.123456+03:00"),
218 Some(1_700_000_000_123)
219 );
220 assert_eq!(
221 millis_from_iso8601("2023-11-14T17:13:20.5-05:00"),
222 Some(1_700_000_000_500)
223 );
224 assert_eq!(millis_from_iso8601("2023-11-14T22:13:20+0300"), None);
225 }
226}