1use alloc::string::String;
9use core::fmt::Write as _;
10
11pub fn parse(value: &str) -> Option<u64> {
18 let (date, rest) = value.split_once('T')?;
19 let time = rest.strip_suffix('Z')?;
20
21 let mut date_parts = date.split('-');
22 let year: i64 = date_parts.next()?.parse().ok()?;
23 let month: u32 = date_parts.next()?.parse().ok()?;
24 let day: u32 = date_parts.next()?.parse().ok()?;
25 if date_parts.next().is_some()
28 || !(1..=9999).contains(&year)
29 || !(1..=12).contains(&month)
30 || !(1..=31).contains(&day)
31 {
32 return None;
33 }
34
35 let (clock, fraction) = time.split_once('.').unwrap_or((time, "0"));
36 let mut clock_parts = clock.split(':');
37 let hour: u64 = clock_parts.next()?.parse().ok()?;
38 let minute: u64 = clock_parts.next()?.parse().ok()?;
39 let second: u64 = clock_parts.next()?.parse().ok()?;
40 if clock_parts.next().is_some() || hour > 23 || minute > 59 || second > 60 {
41 return None;
42 }
43
44 let millis = millis_from_fraction(fraction)?;
47
48 let days = days_from_civil(year, month, day);
49 let seconds = days
50 .checked_mul(86_400)?
51 .checked_add(i64::try_from(hour * 3600 + minute * 60 + second).ok()?)?;
52 u64::try_from(seconds.checked_mul(1000)?.checked_add(i64::from(millis))?).ok()
53}
54
55pub fn format(unix_ms: u64) -> String {
60 let (seconds, millis) = (unix_ms / 1000, unix_ms % 1000);
61 let (days, second_of_day) = (seconds / 86_400, seconds % 86_400);
62 let (year, month, day) = civil_from_days(days);
63 let mut out = String::new();
64 let _ = write!(
65 out,
66 "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{millis:03}Z",
67 second_of_day / 3600,
68 (second_of_day / 60) % 60,
69 second_of_day % 60,
70 );
71 out
72}
73
74fn millis_from_fraction(fraction: &str) -> Option<u16> {
75 if !fraction.bytes().all(|b| b.is_ascii_digit()) {
76 return None;
77 }
78 let digits = fraction.get(..3).unwrap_or(fraction);
79 let value: u16 = digits.parse().ok()?;
80 Some(match digits.len() {
81 0 => 0,
82 1 => value * 100,
83 2 => value * 10,
84 _ => value,
85 })
86}
87
88fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
93 let year = if month <= 2 { year - 1 } else { year };
94 let era = if year >= 0 { year } else { year - 399 } / 400;
95 let year_of_era = year - era * 400;
96 let month = i64::from(month);
97 let shifted_month = if month > 2 { month - 3 } else { month + 9 };
98 let day_of_year = (153 * shifted_month + 2) / 5 + i64::from(day) - 1;
99 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
100 era * 146_097 + day_of_era - 719_468
101}
102
103fn civil_from_days(days: u64) -> (u64, u64, u64) {
108 let era_days = days + 719_468;
109 let era = era_days / 146_097;
110 let day_of_era = era_days % 146_097;
111 let year_of_era =
112 (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
113 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
114 let shifted_month = (5 * day_of_year + 2) / 153;
115 let day = day_of_year - (153 * shifted_month + 2) / 5 + 1;
116 let month = if shifted_month < 10 {
117 shifted_month + 3
118 } else {
119 shifted_month - 9
120 };
121 let year = year_of_era + era * 400 + u64::from(month <= 2);
122 (year, month, day)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn reads_the_epoch() {
131 assert_eq!(parse("1970-01-01T00:00:00.000Z"), Some(0));
132 }
133
134 #[test]
135 fn reads_a_known_instant() {
136 assert_eq!(parse("2026-09-06T10:00:00.000Z"), Some(1_788_688_800_000));
137 }
138
139 #[test]
140 fn keeps_the_milliseconds() {
141 let base = parse("2026-09-06T10:00:00.000Z").expect("valid");
142 assert_eq!(parse("2026-09-06T10:00:00.123Z"), Some(base + 123));
143 }
144
145 #[test]
146 fn scales_a_fraction_that_is_not_three_digits() {
147 let base = parse("2026-09-06T10:00:00.000Z").expect("valid");
148 assert_eq!(parse("2026-09-06T10:00:00.5Z"), Some(base + 500));
149 assert_eq!(parse("2026-09-06T10:00:00.05Z"), Some(base + 50));
150 assert_eq!(parse("2026-09-06T10:00:00.123456Z"), Some(base + 123));
151 }
152
153 #[test]
154 fn a_missing_fraction_is_allowed() {
155 assert_eq!(
156 parse("2026-09-06T10:00:00Z"),
157 parse("2026-09-06T10:00:00.000Z")
158 );
159 }
160
161 #[test]
162 fn handles_a_leap_day() {
163 let feb = parse("2024-02-29T00:00:00.000Z").expect("2024 is a leap year");
164 let mar = parse("2024-03-01T00:00:00.000Z").expect("valid");
165 assert_eq!(mar - feb, 86_400_000);
166 }
167
168 #[test]
169 fn handles_the_four_hundred_year_rule() {
170 assert_eq!(
174 days_from_civil(1900, 3, 1) - days_from_civil(1900, 2, 28),
175 1,
176 "1900 is divisible by 100 and not by 400, so it has no 29th"
177 );
178 assert_eq!(
179 days_from_civil(2000, 3, 1) - days_from_civil(2000, 2, 28),
180 2
181 );
182 }
183
184 #[test]
185 fn refuses_a_year_the_calendar_arithmetic_cannot_hold() {
186 assert_eq!(parse("99999999999999999-01-01T00:00:00.000Z"), None);
188 assert_eq!(parse("-4713-01-01T00:00:00.000Z"), None);
189 assert_eq!(parse("10000-01-01T00:00:00.000Z"), None);
190 }
191
192 #[test]
193 fn refuses_an_instant_before_the_epoch() {
194 assert_eq!(parse("1969-12-31T23:59:59.000Z"), None);
195 }
196
197 #[test]
198 fn accepts_a_leap_second() {
199 assert!(parse("2016-12-31T23:59:60.000Z").is_some());
200 }
201
202 #[test]
203 fn days_advance_by_exactly_one_day() {
204 let a = parse("2026-09-06T00:00:00.000Z").expect("valid");
205 let b = parse("2026-09-07T00:00:00.000Z").expect("valid");
206 assert_eq!(b - a, 86_400_000);
207 }
208
209 #[test]
210 fn writes_the_epoch() {
211 assert_eq!(format(0), "1970-01-01T00:00:00.000Z");
212 }
213
214 #[test]
215 fn writes_a_known_instant() {
216 assert_eq!(format(1_788_688_800_123), "2026-09-06T10:00:00.123Z");
217 }
218
219 #[test]
220 fn pads_every_field() {
221 assert_eq!(format(1_041_816_065_007), "2003-01-06T01:21:05.007Z");
222 }
223
224 #[test]
225 fn writes_a_leap_day() {
226 assert_eq!(format(1_709_164_800_000), "2024-02-29T00:00:00.000Z");
227 }
228
229 #[test]
230 fn round_trips_through_parse() {
231 for ms in [
232 0,
233 1,
234 999,
235 86_399_999,
236 86_400_000,
237 951_782_400_000,
238 1_788_688_800_123,
239 253_402_300_799_999,
240 ] {
241 let text = format(ms);
242 assert_eq!(parse(&text), Some(ms), "{text} should read back as {ms}");
243 }
244 }
245
246 #[test]
247 fn refuses_anything_malformed() {
248 assert_eq!(parse(""), None);
249 assert_eq!(parse("2026-09-06"), None, "a date with no time");
250 assert_eq!(parse("2026-09-06T10:00:00.000"), None, "no zone marker");
251 assert_eq!(
252 parse("2026-09-06T10:00:00.000+01:00"),
253 None,
254 "only UTC is defined"
255 );
256 assert_eq!(
257 parse("2026-13-06T10:00:00.000Z"),
258 None,
259 "month out of range"
260 );
261 assert_eq!(parse("2026-09-06T24:00:00.000Z"), None, "hour out of range");
262 assert_eq!(parse("2026-09-06T10:00.000Z"), None, "no seconds");
263 assert_eq!(parse("not-a-date"), None);
264 assert_eq!(
265 parse("2026-09-06T10:00:00.abcZ"),
266 None,
267 "a non-numeric fraction"
268 );
269 }
270}