|
| 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 { |
| 29 | + 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 { |
| 52 | + 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() <= 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() <= 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] |
| 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", i32::MAX as i64, 0); |
| 191 | + case("2038-01-19T03:14:08.000000Z", 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", i64::MAX, 0); |
| 201 | + case("+292277026596-12-04T15:30:06.000000Z", 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 | +} |
0 commit comments