1 use std::fmt;
2 
3 /// A date/time type which exists primarily to convert `SystemTime` timestamps into an ISO 8601
4 /// formatted string.
5 ///
6 /// Yes, this exists. Before you have a heart attack, understand that the meat of this is musl's
7 /// [`__secs_to_tm`][1] converted to Rust via [c2rust][2] and then cleaned up by hand. All existing
8 /// `strftime`-like APIs I found were unable to handle the full range of timestamps representable
9 /// by `SystemTime`, including `strftime` itself, since tm.tm_year is an int.
10 ///
11 /// TODO: figure out how to properly attribute the MIT licensed musl project.
12 ///
13 /// [1] http://git.musl-libc.org/cgit/musl/tree/src/time/__secs_to_tm.c
14 /// [2] https://c2rust.com/
15 ///
16 /// This is directly copy-pasted from https://github.com/danburkert/kudu-rs/blob/c9660067e5f4c1a54143f169b5eeb49446f82e54/src/timestamp.rs#L5-L18
17 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
18 pub(crate) struct DateTime {
19     year: i64,
20     month: u8,
21     day: u8,
22     hour: u8,
23     minute: u8,
24     second: u8,
25     nanos: u32,
26 }
27 
28 impl fmt::Display for DateTime {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result29     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30         if self.year > 9999 {
31             write!(f, "+{}", self.year)?;
32         } else if self.year < 0 {
33             write!(f, "{:05}", self.year)?;
34         } else {
35             write!(f, "{:04}", self.year)?;
36         }
37 
38         write!(
39             f,
40             "-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
41             self.month,
42             self.day,
43             self.hour,
44             self.minute,
45             self.second,
46             self.nanos / 1_000
47         )
48     }
49 }
50 
51 impl From<std::time::SystemTime> for DateTime {
from(timestamp: std::time::SystemTime) -> DateTime52     fn from(timestamp: std::time::SystemTime) -> DateTime {
53         let (t, nanos) = match timestamp.duration_since(std::time::UNIX_EPOCH) {
54             Ok(duration) => {
55                 debug_assert!(duration.as_secs() <= std::i64::MAX as u64);
56                 (duration.as_secs() as i64, duration.subsec_nanos())
57             }
58             Err(error) => {
59                 let duration = error.duration();
60                 debug_assert!(duration.as_secs() <= std::i64::MAX as u64);
61                 let (secs, nanos) = (duration.as_secs() as i64, duration.subsec_nanos());
62                 if nanos == 0 {
63                     (-secs, 0)
64                 } else {
65                     (-secs - 1, 1_000_000_000 - nanos)
66                 }
67             }
68         };
69 
70         // 2000-03-01 (mod 400 year, immediately after feb29
71         const LEAPOCH: i64 = 946_684_800 + 86400 * (31 + 29);
72         const DAYS_PER_400Y: i32 = 365 * 400 + 97;
73         const DAYS_PER_100Y: i32 = 365 * 100 + 24;
74         const DAYS_PER_4Y: i32 = 365 * 4 + 1;
75         static DAYS_IN_MONTH: [i8; 12] = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
76 
77         // Note(dcb): this bit is rearranged slightly to avoid integer overflow.
78         let mut days: i64 = (t / 86_400) - (LEAPOCH / 86_400);
79         let mut remsecs: i32 = (t % 86_400) as i32;
80         if remsecs < 0i32 {
81             remsecs += 86_400;
82             days -= 1
83         }
84 
85         let mut qc_cycles: i32 = (days / i64::from(DAYS_PER_400Y)) as i32;
86         let mut remdays: i32 = (days % i64::from(DAYS_PER_400Y)) as i32;
87         if remdays < 0 {
88             remdays += DAYS_PER_400Y;
89             qc_cycles -= 1;
90         }
91 
92         let mut c_cycles: i32 = remdays / DAYS_PER_100Y;
93         if c_cycles == 4 {
94             c_cycles -= 1;
95         }
96         remdays -= c_cycles * DAYS_PER_100Y;
97 
98         let mut q_cycles: i32 = remdays / DAYS_PER_4Y;
99         if q_cycles == 25 {
100             q_cycles -= 1;
101         }
102         remdays -= q_cycles * DAYS_PER_4Y;
103 
104         let mut remyears: i32 = remdays / 365;
105         if remyears == 4 {
106             remyears -= 1;
107         }
108         remdays -= remyears * 365;
109 
110         let mut years: i64 = i64::from(remyears)
111             + 4 * i64::from(q_cycles)
112             + 100 * i64::from(c_cycles)
113             + 400 * i64::from(qc_cycles);
114 
115         let mut months: i32 = 0;
116         while i32::from(DAYS_IN_MONTH[months as usize]) <= remdays {
117             remdays -= i32::from(DAYS_IN_MONTH[months as usize]);
118             months += 1
119         }
120 
121         if months >= 10 {
122             months -= 12;
123             years += 1;
124         }
125 
126         DateTime {
127             year: years + 2000,
128             month: (months + 3) as u8,
129             day: (remdays + 1) as u8,
130             hour: (remsecs / 3600) as u8,
131             minute: (remsecs / 60 % 60) as u8,
132             second: (remsecs % 60) as u8,
133             nanos,
134         }
135     }
136 }
137 
138 #[cfg(test)]
139 mod tests {
140     use std::i32;
141     use std::time::{Duration, UNIX_EPOCH};
142 
143     use super::*;
144 
145     #[test]
test_datetime()146     fn test_datetime() {
147         let case = |expected: &str, secs: i64, micros: u32| {
148             let timestamp = if secs >= 0 {
149                 UNIX_EPOCH + Duration::new(secs as u64, micros * 1_000)
150             } else {
151                 (UNIX_EPOCH - Duration::new(!secs as u64 + 1, 0)) + Duration::new(0, micros * 1_000)
152             };
153             assert_eq!(
154                 expected,
155                 format!("{}", DateTime::from(timestamp)),
156                 "secs: {}, micros: {}",
157                 secs,
158                 micros
159             )
160         };
161 
162         // Mostly generated with:
163         //  - date -jur <secs> +"%Y-%m-%dT%H:%M:%S.000000Z"
164         //  - http://unixtimestamp.50x.eu/
165 
166         case("1970-01-01T00:00:00.000000Z", 0, 0);
167 
168         case("1970-01-01T00:00:00.000001Z", 0, 1);
169         case("1970-01-01T00:00:00.500000Z", 0, 500_000);
170         case("1970-01-01T00:00:01.000001Z", 1, 1);
171         case("1970-01-01T00:01:01.000001Z", 60 + 1, 1);
172         case("1970-01-01T01:01:01.000001Z", 60 * 60 + 60 + 1, 1);
173         case(
174             "1970-01-02T01:01:01.000001Z",
175             24 * 60 * 60 + 60 * 60 + 60 + 1,
176             1,
177         );
178 
179         case("1969-12-31T23:59:59.000000Z", -1, 0);
180         case("1969-12-31T23:59:59.000001Z", -1, 1);
181         case("1969-12-31T23:59:59.500000Z", -1, 500_000);
182         case("1969-12-31T23:58:59.000001Z", -60 - 1, 1);
183         case("1969-12-31T22:58:59.000001Z", -60 * 60 - 60 - 1, 1);
184         case(
185             "1969-12-30T22:58:59.000001Z",
186             -24 * 60 * 60 - 60 * 60 - 60 - 1,
187             1,
188         );
189 
190         case("2038-01-19T03:14:07.000000Z", std::i32::MAX as i64, 0);
191         case("2038-01-19T03:14:08.000000Z", std::i32::MAX as i64 + 1, 0);
192         case("1901-12-13T20:45:52.000000Z", i32::MIN as i64, 0);
193         case("1901-12-13T20:45:51.000000Z", i32::MIN as i64 - 1, 0);
194 
195         // Skipping these tests on windows as std::time::SysteTime range is low
196         // on Windows compared with that of Unix which can cause the following
197         // high date value tests to panic
198         #[cfg(not(target_os = "windows"))]
199         {
200             case("+292277026596-12-04T15:30:07.000000Z", std::i64::MAX, 0);
201             case("+292277026596-12-04T15:30:06.000000Z", std::i64::MAX - 1, 0);
202             case("-292277022657-01-27T08:29:53.000000Z", i64::MIN + 1, 0);
203         }
204 
205         case("1900-01-01T00:00:00.000000Z", -2208988800, 0);
206         case("1899-12-31T23:59:59.000000Z", -2208988801, 0);
207         case("0000-01-01T00:00:00.000000Z", -62167219200, 0);
208         case("-0001-12-31T23:59:59.000000Z", -62167219201, 0);
209 
210         case("1234-05-06T07:08:09.000000Z", -23215049511, 0);
211         case("-1234-05-06T07:08:09.000000Z", -101097651111, 0);
212         case("2345-06-07T08:09:01.000000Z", 11847456541, 0);
213         case("-2345-06-07T08:09:01.000000Z", -136154620259, 0);
214     }
215 }
216