1use std::fmt;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
16pub struct DateTime {
17 micros: i64,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ParseError(&'static str);
22
23impl fmt::Display for ParseError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 write!(f, "invalid date-time: {}", self.0)
26 }
27}
28
29impl std::error::Error for ParseError {}
30
31impl DateTime {
32 pub const UNIX_EPOCH: DateTime = DateTime { micros: 0 };
33
34 pub fn now() -> Self {
35 let d = SystemTime::now()
36 .duration_since(UNIX_EPOCH)
37 .unwrap_or_default();
38 DateTime {
39 micros: d.as_micros() as i64,
40 }
41 }
42
43 pub const fn from_unix_micros(micros: i64) -> Self {
44 DateTime { micros }
45 }
46
47 pub const fn from_unix_seconds(secs: i64) -> Self {
48 DateTime {
49 micros: secs * 1_000_000,
50 }
51 }
52
53 pub const fn unix_micros(&self) -> i64 {
54 self.micros
55 }
56
57 pub const fn unix_seconds(&self) -> i64 {
58 self.micros.div_euclid(1_000_000)
59 }
60
61 pub fn add_seconds(&self, secs: i64) -> Self {
62 DateTime {
63 micros: self.micros + secs * 1_000_000,
64 }
65 }
66
67 pub fn from_utc_fields(
69 year: i64,
70 month: u32,
71 day: u32,
72 hour: u32,
73 min: u32,
74 sec: u32,
75 micros: u32,
76 ) -> Option<Self> {
77 if !(1..=12).contains(&month)
78 || day == 0
79 || day > days_in_month(year, month)
80 || hour > 23
81 || min > 59
82 || sec > 60
83 || micros > 999_999
84 {
85 return None;
86 }
87 let days = days_from_civil(year, month, day);
89 let secs = days * 86_400 + hour as i64 * 3_600 + min as i64 * 60 + sec as i64;
90 Some(DateTime {
91 micros: secs * 1_000_000 + micros as i64,
92 })
93 }
94
95 pub fn to_utc_fields(&self) -> (i64, u32, u32, u32, u32, u32, u32) {
97 let secs = self.micros.div_euclid(1_000_000);
98 let micros = self.micros.rem_euclid(1_000_000) as u32;
99 let days = secs.div_euclid(86_400);
100 let sod = secs.rem_euclid(86_400) as u32;
101 let (y, m, d) = civil_from_days(days);
102 (y, m, d, sod / 3_600, (sod % 3_600) / 60, sod % 60, micros)
103 }
104
105 pub fn parse(s: &str) -> Result<Self, ParseError> {
106 let b = s.trim().as_bytes();
107 let mut p = Cursor { b, i: 0 };
108 let year = p.digits(4).ok_or(ParseError("year"))? as i64;
109 p.expect(b'-')
110 .ok_or(ParseError("expected '-' after year"))?;
111 let month = p.digits(2).ok_or(ParseError("month"))?;
112 p.expect(b'-')
113 .ok_or(ParseError("expected '-' after month"))?;
114 let day = p.digits(2).ok_or(ParseError("day"))?;
115 match p.next() {
116 Some(b'T' | b't' | b' ') => {}
117 _ => return Err(ParseError("expected 'T' separator")),
118 }
119 let hour = p.digits(2).ok_or(ParseError("hour"))?;
120 p.expect(b':')
121 .ok_or(ParseError("expected ':' after hour"))?;
122 let min = p.digits(2).ok_or(ParseError("minute"))?;
123 let sec = if p.peek() == Some(b':') {
125 p.i += 1;
126 p.digits(2).ok_or(ParseError("second"))?
127 } else {
128 0
129 };
130 let mut micros = 0u32;
131 if matches!(p.peek(), Some(b'.' | b',')) {
132 p.i += 1;
133 let start = p.i;
134 let mut scale = 100_000u32;
135 while let Some(c @ b'0'..=b'9') = p.peek() {
136 micros += (c - b'0') as u32 * scale;
137 scale /= 10;
138 p.i += 1;
139 }
140 if p.i == start {
141 return Err(ParseError("empty fraction"));
142 }
143 }
144 let offset_secs: i64 = match p.next() {
145 None => 0,
147 Some(b'Z' | b'z') => 0,
148 Some(sign @ (b'+' | b'-')) => {
149 let oh = p.digits(2).ok_or(ParseError("offset hour"))? as i64;
150 if p.peek() == Some(b':') {
151 p.i += 1;
152 }
153 let om = if p.peek().is_some() {
155 p.digits(2).ok_or(ParseError("offset minute"))? as i64
156 } else {
157 0
158 };
159 if oh > 23 || om > 59 {
160 return Err(ParseError("offset out of range"));
161 }
162 let o = oh * 3_600 + om * 60;
163 if sign == b'+' { o } else { -o }
164 }
165 Some(_) => return Err(ParseError("unexpected character")),
166 };
167 if p.i != b.len() {
168 return Err(ParseError("trailing characters"));
169 }
170 let local = DateTime::from_utc_fields(year, month, day, hour, min, sec, micros)
171 .ok_or(ParseError("field out of range"))?;
172 Ok(DateTime {
173 micros: local.micros - offset_secs * 1_000_000,
174 })
175 }
176}
177
178impl fmt::Display for DateTime {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 let (y, mo, d, h, mi, s, us) = self.to_utc_fields();
181 write!(
182 f,
183 "{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{:03}Z",
184 us / 1_000
185 )
186 }
187}
188
189impl fmt::Debug for DateTime {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 fmt::Display::fmt(self, f)
192 }
193}
194
195impl std::str::FromStr for DateTime {
196 type Err = ParseError;
197 fn from_str(s: &str) -> Result<Self, Self::Err> {
198 DateTime::parse(s)
199 }
200}
201
202impl serde::Serialize for DateTime {
203 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
204 s.collect_str(self)
205 }
206}
207
208impl<'de> serde::Deserialize<'de> for DateTime {
209 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
210 struct V;
211 impl serde::de::Visitor<'_> for V {
212 type Value = DateTime;
213 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 f.write_str("an RFC 3339 date-time string")
215 }
216 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<DateTime, E> {
217 DateTime::parse(v)
218 .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &self))
219 }
220 }
221 d.deserialize_str(V)
222 }
223}
224
225struct Cursor<'a> {
226 b: &'a [u8],
227 i: usize,
228}
229
230impl Cursor<'_> {
231 fn peek(&self) -> Option<u8> {
232 self.b.get(self.i).copied()
233 }
234 fn next(&mut self) -> Option<u8> {
235 let c = self.peek()?;
236 self.i += 1;
237 Some(c)
238 }
239 fn expect(&mut self, c: u8) -> Option<()> {
240 (self.next()? == c).then_some(())
241 }
242 fn digits(&mut self, n: usize) -> Option<u32> {
243 let mut v = 0u32;
244 for _ in 0..n {
245 let c = self.next()?;
246 if !c.is_ascii_digit() {
247 return None;
248 }
249 v = v * 10 + (c - b'0') as u32;
250 }
251 Some(v)
252 }
253}
254
255fn is_leap(y: i64) -> bool {
256 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
257}
258
259fn days_in_month(y: i64, m: u32) -> u32 {
260 match m {
261 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
262 4 | 6 | 9 | 11 => 30,
263 2 if is_leap(y) => 29,
264 2 => 28,
265 _ => 0,
266 }
267}
268
269fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
271 let y = if m <= 2 { y - 1 } else { y };
272 let era = y.div_euclid(400);
273 let yoe = y - era * 400;
274 let m = m as i64;
275 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d as i64 - 1;
276 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
277 era * 146_097 + doe - 719_468
278}
279
280fn civil_from_days(z: i64) -> (i64, u32, u32) {
281 let z = z + 719_468;
282 let era = z.div_euclid(146_097);
283 let doe = z - era * 146_097;
284 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
285 let y = yoe + era * 400;
286 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
287 let mp = (5 * doy + 2) / 153;
288 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
289 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
290 (if m <= 2 { y + 1 } else { y }, m, d)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn roundtrip_and_formats() {
299 let t = DateTime::parse("2013-02-01T20:53:32.486Z").unwrap();
300 assert_eq!(t.to_string(), "2013-02-01T20:53:32.486Z");
301 assert_eq!(t.unix_seconds(), 1_359_752_012);
302 let o = DateTime::parse("2013-02-01T22:53:32.486+02:00").unwrap();
303 assert_eq!(t, o);
304 assert_eq!(
305 DateTime::parse("2013-02-01T20:53:32")
306 .unwrap()
307 .unix_seconds(),
308 1_359_752_012
309 );
310 assert_eq!(
311 DateTime::parse("2013-02-01t20:53:32.123456789z")
312 .unwrap()
313 .unix_micros()
314 % 1_000_000,
315 123_456
316 );
317 assert_eq!(
318 DateTime::parse("1970-01-01T00:00:00Z").unwrap(),
319 DateTime::UNIX_EPOCH
320 );
321 assert_eq!(
322 DateTime::parse("2024-02-29T00:00:00Z").unwrap().to_string(),
323 "2024-02-29T00:00:00.000Z"
324 );
325 assert_eq!(
326 DateTime::parse("1969-12-31T23:59:59Z")
327 .unwrap()
328 .unix_seconds(),
329 -1
330 );
331 assert_eq!(DateTime::parse("2013-02-01 21:53:32.486+01").unwrap(), t);
332 }
333
334 #[test]
335 fn rejects_garbage() {
336 for s in [
337 "",
338 "2023-13-01T00:00:00Z",
339 "2023-02-29T00:00:00Z",
340 "2023-01-01",
341 "2023-01-01T00:00:00Zjunk",
342 "2023-01-01T00:00:00.Z",
343 ] {
344 assert!(DateTime::parse(s).is_err(), "{s}");
345 }
346 }
347
348 #[test]
349 fn calendar_consistency() {
350 for days in (-800_000i64..800_000).step_by(997) {
351 let (y, m, d) = civil_from_days(days);
352 assert_eq!(days_from_civil(y, m, d), days);
353 }
354 }
355}