1use crate::fields::{date, first_field};
8use std::{cmp::Ordering, fmt};
9
10pub static DOW: &[&str] = &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
13pub static MONTH: &[&str] = &[
15 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
16];
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct DateTime {
22 pub year: u16,
24 pub month: u8,
26 pub day: u8,
28 pub hour: u8,
30 pub minute: u8,
32 pub second: u8,
34 pub tz_before_gmt: bool,
36 pub tz_hour: u8,
38 pub tz_minute: u8,
40}
41
42const RFC3339_WIDTHS: [u32; 8] = [4, 2, 2, 2, 2, 2, 2, 2];
43
44impl DateTime {
45 pub fn parse_rfc822(value: &str) -> Option<Self> {
47 let bytes = value.as_bytes();
48 date::parse_bytes(bytes.get(first_field(bytes, 0..bytes.len()))?)
49 }
50
51 pub fn parse_rfc3339(value: &str) -> Option<Self> {
53 let mut pos = 0usize;
54 let mut parts = [0u32; 8];
55 let mut widths = RFC3339_WIDTHS;
56 let mut skip_digits = false;
57 let mut is_plus = true;
58
59 for &ch in value.as_bytes() {
60 match ch {
61 b'0'..=b'9' if !skip_digits => {
62 let width = widths.get_mut(pos)?;
63 if *width == 0 {
64 return None;
65 }
66 *width -= 1;
67 *parts.get_mut(pos)? += (ch - b'0') as u32 * 10u32.pow(*width);
68 }
69 b'-' if pos <= 1 => pos += 1,
70 b'-' if pos == 5 => {
71 pos += 1;
72 is_plus = false;
73 skip_digits = false;
74 }
75 b'T' if pos == 2 => pos += 1,
76 b':' if matches!(pos, 3 | 4 | 6) => pos += 1,
77 b'+' if pos == 5 => {
78 pos += 1;
79 skip_digits = false;
80 }
81 b'.' if pos == 5 => skip_digits = true,
82 b'-' | b'T' | b':' | b'+' | b'.' => return None,
83 _ => (),
84 }
85 }
86
87 let [year, month, day, hour, minute, second, tz_hour, tz_minute] = parts;
88 (pos >= 5).then_some(DateTime {
89 year: year as u16,
90 month: month as u8,
91 day: day as u8,
92 hour: hour as u8,
93 minute: minute as u8,
94 second: second as u8,
95 tz_hour: tz_hour as u8,
96 tz_minute: tz_minute as u8,
97 tz_before_gmt: !is_plus,
98 })
99 }
100
101 fn zone_sign(&self) -> &'static str {
102 if self.tz_before_gmt && (self.tz_hour > 0 || self.tz_minute > 0) {
103 "-"
104 } else {
105 "+"
106 }
107 }
108
109 pub fn to_rfc822(&self) -> String {
111 format!(
112 "{}, {} {} {:04} {:02}:{:02}:{:02} {}{:02}{:02}",
113 DOW.get(self.day_of_week() as usize)
114 .copied()
115 .unwrap_or_default(),
116 self.day,
117 MONTH
118 .get(self.month.saturating_sub(1) as usize)
119 .copied()
120 .unwrap_or_default(),
121 self.year,
122 self.hour,
123 self.minute,
124 self.second,
125 self.zone_sign(),
126 self.tz_hour,
127 self.tz_minute
128 )
129 }
130
131 pub fn to_rfc3339(&self) -> String {
133 if self.tz_hour != 0 || self.tz_minute != 0 {
134 format!(
135 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
136 self.year,
137 self.month,
138 self.day,
139 self.hour,
140 self.minute,
141 self.second,
142 self.zone_sign(),
143 self.tz_hour,
144 self.tz_minute
145 )
146 } else {
147 format!(
148 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
149 self.year, self.month, self.day, self.hour, self.minute, self.second,
150 )
151 }
152 }
153
154 pub fn is_valid(&self) -> bool {
156 (0..=23).contains(&self.tz_hour)
157 && (1900..=3000).contains(&self.year)
158 && (0..=59).contains(&self.tz_minute)
159 && (1..=12).contains(&self.month)
160 && (1..=31).contains(&self.day)
161 && (0..=23).contains(&self.hour)
162 && (0..=59).contains(&self.minute)
163 && (0..=59).contains(&self.second)
164 }
165
166 pub fn to_timestamp(&self) -> i64 {
168 self.to_timestamp_local()
169 + ((self.tz_hour as i64 * 3600 + self.tz_minute as i64 * 60)
170 * if self.tz_before_gmt { 1 } else { -1 })
171 }
172
173 pub fn to_timestamp_local(&self) -> i64 {
175 let month = self.month as u32;
176 let year_base = 4800;
177 let m_adj = month.wrapping_sub(3);
178 let carry = i64::from(m_adj > month);
179 let adjust = if carry > 0 { 12 } else { 0 };
180 let y_adj = self.year as i64 + year_base - carry;
181 let month_days = ((m_adj.wrapping_add(adjust)) * 62719 + 769) / 2048;
182 let leap_days = y_adj / 4 - y_adj / 100 + y_adj / 400;
183 (y_adj * 365 + leap_days + month_days as i64 + (self.day as i64 - 1) - 2472632) * 86400
184 + self.hour as i64 * 3600
185 + self.minute as i64 * 60
186 + self.second as i64
187 }
188
189 pub fn from_timestamp(timestamp: i64) -> Self {
191 let (z, seconds) = (
192 (timestamp.div_euclid(86400)) + 719468,
193 timestamp.rem_euclid(86400),
194 );
195 let era: i64 = (if z >= 0 { z } else { z - 146096 }) / 146097;
196 let doe: u64 = (z - era * 146097) as u64;
197 let yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
198 let y: i64 = (yoe as i64) + era * 400;
199 let doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100);
200 let mp = (5 * doy + 2) / 153;
201 let d: u64 = doy - (153 * mp + 2) / 5 + 1;
202 let m: u64 = if mp < 10 { mp + 3 } else { mp - 9 };
203 let (h, mn, s) = (seconds / 3600, (seconds / 60) % 60, seconds % 60);
204
205 DateTime {
206 year: (y + i64::from(m <= 2)) as u16,
207 month: m as u8,
208 day: d as u8,
209 hour: h as u8,
210 minute: mn as u8,
211 second: s as u8,
212 tz_before_gmt: false,
213 tz_hour: 0,
214 tz_minute: 0,
215 }
216 }
217
218 pub fn day_of_week(&self) -> u8 {
220 (((self.to_timestamp_local() as f64 / 86400.0).floor() as i64 + 4).rem_euclid(7)) as u8
221 }
222
223 pub fn julian_day(&self) -> i64 {
225 let day = self.day as i64;
226 let (month, year) = if self.month > 2 {
227 ((self.month - 3) as i64, self.year as i64)
228 } else {
229 ((self.month + 9) as i64, (self.year as i64 - 1))
230 };
231
232 let c = year / 100;
233 c * 146097 / 4 + (year - c * 100) * 1461 / 4 + (month * 153 + 2) / 5 + day + 1721119
234 }
235
236 pub fn to_timezone(&self, tz: i64) -> DateTime {
239 let mut dt = DateTime::from_timestamp(self.to_timestamp().saturating_add(tz));
240 dt.tz_before_gmt = tz < 0;
241 let offset = tz.unsigned_abs();
242 dt.tz_hour = u8::try_from(offset / 3600).unwrap_or(u8::MAX);
243 dt.tz_minute = ((offset % 3600) / 60) as u8;
244 dt
245 }
246}
247
248impl PartialOrd for DateTime {
249 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
250 Some(self.cmp(other))
251 }
252}
253
254impl Ord for DateTime {
255 fn cmp(&self, other: &Self) -> Ordering {
256 self.to_timestamp().cmp(&other.to_timestamp())
257 }
258}
259
260impl fmt::Display for DateTime {
261 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
262 fmt.write_str(&self.to_rfc3339())
263 }
264}
265
266impl From<DateTime> for i64 {
267 fn from(value: DateTime) -> Self {
268 value.to_timestamp()
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::DateTime;
275
276 #[test]
277 fn rfc3339_round_trip() {
278 for input in [
279 "2021-11-20T14:22:01-08:00",
280 "1969-02-13T23:32:00-03:30",
281 "2004-06-28T23:43:45Z",
282 ] {
283 let date = DateTime::parse_rfc3339(input).expect("valid");
284 assert_eq!(date.to_rfc3339(), input);
285 }
286 assert_eq!(
287 DateTime::parse_rfc3339("2004-06-28T23:43:45.000Z").map(|d| d.to_rfc3339()),
288 Some("2004-06-28T23:43:45Z".to_string())
289 );
290 assert!(DateTime::parse_rfc3339("2004-06-28+23:43:45").is_none());
291 }
292
293 #[test]
294 fn timestamps() {
295 let date = DateTime::parse_rfc3339("2021-11-20T14:22:01-08:00").expect("valid");
296 assert_eq!(date.to_timestamp(), 1637446921);
297 assert_eq!(
298 DateTime::from_timestamp(1637446921).to_rfc3339(),
299 "2021-11-20T22:22:01Z"
300 );
301 assert_eq!(date.to_rfc822(), "Sat, 20 Nov 2021 14:22:01 -0800");
302 assert_eq!(date.day_of_week(), 6);
303 assert!(date.is_valid());
304 assert_eq!(i64::from(date), 1637446921);
305 assert_eq!(
306 date.to_timezone(3600).to_rfc3339(),
307 "2021-11-20T23:22:01+01:00"
308 );
309 assert_eq!(date.julian_day(), 2459539);
310 }
311
312 #[test]
313 fn extreme_timezones_saturate() {
314 let date = DateTime::parse_rfc3339("2021-11-20T14:22:01-08:00").expect("valid");
315 for tz in [i64::MIN, i64::MIN + 1, i64::MAX] {
316 let moved = date.to_timezone(tz);
317 assert_eq!(moved.tz_before_gmt, tz < 0);
318 assert_eq!(moved.tz_hour, u8::MAX);
319 assert!(moved.tz_minute < 60);
320 }
321 let moved = date.to_timezone(-(5 * 3600 + 30 * 60));
322 assert_eq!(moved.to_rfc3339(), "2021-11-20T16:52:01-05:30");
323 }
324}