1pub fn http_date(unix: i64) -> String {
12 const DAYS: [&str; 7] = ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"];
13 const MONTHS: [&str; 12] = [
14 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
15 ];
16
17 let days_since_epoch = unix.div_euclid(86_400);
18 let seconds_of_day = unix.rem_euclid(86_400);
19 let (year, month, day) = civil_from_days(days_since_epoch);
20
21 format!(
22 "{}, {:02} {} {} {:02}:{:02}:{:02} GMT",
23 DAYS[(days_since_epoch.rem_euclid(7)) as usize],
24 day,
25 MONTHS[(month - 1) as usize],
26 year,
27 seconds_of_day / 3600,
28 (seconds_of_day % 3600) / 60,
29 seconds_of_day % 60,
30 )
31}
32
33fn civil_from_days(days: i64) -> (i64, u32, u32) {
38 let z = days + 719_468;
39 let era = z.div_euclid(146_097);
40 let day_of_era = z.rem_euclid(146_097);
41 let year_of_era =
42 (day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
43 let year = year_of_era + era * 400;
44 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
45 let mp = (5 * day_of_year + 2) / 153;
46 let day = (day_of_year - (153 * mp + 2) / 5 + 1) as u32;
47 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
48 (year + i64::from(month <= 2), month, day)
49}
50
51
52pub fn parse_http_date(text: &str) -> Option<i64> {
57 let text = text.trim();
58 let parts: Vec<&str> = text.split_whitespace().collect();
59
60 let (day, month, year, clock) = match parts.as_slice() {
61 [weekday, day, month, year, clock, "GMT"] if weekday.ends_with(',') => {
63 (day.parse::<u32>().ok()?, month_number(month)?, year.parse::<i64>().ok()?, *clock)
64 }
65 [weekday, date, clock, "GMT"] if weekday.ends_with(',') => {
67 let mut pieces = date.split('-');
68 let day = pieces.next()?.parse::<u32>().ok()?;
69 let month = month_number(pieces.next()?)?;
70 let year = pieces.next()?.parse::<i64>().ok()?;
71 if pieces.next().is_some() {
72 return None;
73 }
74 let year = if year < 100 { if year < 70 { 2000 + year } else { 1900 + year } } else { year };
78 (day, month, year, *clock)
79 }
80 [_weekday, month, day, clock, year] => {
82 (day.parse::<u32>().ok()?, month_number(month)?, year.parse::<i64>().ok()?, *clock)
83 }
84 _ => return None,
85 };
86
87 let mut clock = clock.split(':');
88 let hour: i64 = clock.next()?.parse().ok()?;
89 let minute: i64 = clock.next()?.parse().ok()?;
90 let second: i64 = clock.next()?.parse().ok()?;
91 if clock.next().is_some() || hour > 23 || minute > 59 || second > 60 {
92 return None;
93 }
94 if day == 0 || day > 31 {
95 return None;
96 }
97
98 let days = days_from_civil(year, month, day);
99 Some(days * 86_400 + hour * 3600 + minute * 60 + second)
100}
101
102pub fn parse_ymd(text: &str) -> Option<i64> {
107 let mut parts = text.trim().split('-');
108 let year: i64 = parts.next()?.parse().ok()?;
109 let month: u32 = parts.next()?.parse().ok()?;
110 let day: u32 = parts.next()?.parse().ok()?;
111 if parts.next().is_some() || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
112 return None;
113 }
114 Some(days_from_civil(year, month, day) * 86_400)
115}
116
117fn month_number(name: &str) -> Option<u32> {
118 const MONTHS: [&str; 12] = [
119 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
120 ];
121 let lower = name.to_ascii_lowercase();
122 MONTHS.iter().position(|m| *m == lower).map(|i| i as u32 + 1)
123}
124
125fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
127 let year = if month <= 2 { year - 1 } else { year };
128 let era = year.div_euclid(400);
129 let year_of_era = year.rem_euclid(400);
130 let month = i64::from(month);
131 let day_of_year = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + i64::from(day) - 1;
132 let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
133 era * 146_097 + day_of_era - 719_468
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn formats_known_http_dates() {
142 assert_eq!(http_date(0), "Thu, 01 Jan 1970 00:00:00 GMT");
143 assert_eq!(http_date(1_000_000_000), "Sun, 09 Sep 2001 01:46:40 GMT");
144 assert_eq!(http_date(784_111_777), "Sun, 06 Nov 1994 08:49:37 GMT");
145 }
146
147 #[test]
148 fn parses_all_three_formats_to_the_same_instant() {
149 assert_eq!(parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT"), Some(784_111_777));
151 assert_eq!(parse_http_date("Sunday, 06-Nov-94 08:49:37 GMT"), Some(784_111_777));
152 assert_eq!(parse_http_date("Sun Nov 6 08:49:37 1994"), Some(784_111_777));
153 }
154
155 #[test]
156 fn formatting_and_parsing_round_trip() {
157 for unix in [0, 1, 86_399, 951_782_400, 1_000_000_000, 1_709_164_800, 4_102_444_800] {
158 assert_eq!(parse_http_date(&http_date(unix)), Some(unix), "{unix}");
159 }
160 }
161
162 #[test]
163 fn reads_iso_dates_at_midnight() {
164 assert_eq!(parse_ymd("1970-01-01"), Some(0));
165 assert_eq!(parse_ymd("1994-11-06"), Some(784_111_777 - (8 * 3600 + 49 * 60 + 37)));
166 assert_eq!(parse_ymd("2027-01-01"), Some(1_798_761_600));
167 assert_eq!(parse_ymd("2027-13-01"), None);
168 assert_eq!(parse_ymd("2027-01"), None);
169 assert_eq!(parse_ymd("tomorrow"), None);
170 }
171
172 #[test]
173 fn rejects_what_it_cannot_read() {
174 assert_eq!(parse_http_date(""), None);
175 assert_eq!(parse_http_date("yesterday"), None);
176 assert_eq!(parse_http_date("Sun, 06 Nov 1994 08:49:37 PST"), None);
177 assert_eq!(parse_http_date("Sun, 06 Nov 1994 25:00:00 GMT"), None);
178 assert_eq!(parse_http_date("Sun, 32 Nov 1994 08:49:37 GMT"), None);
179 assert_eq!(parse_http_date("Sun, 06 Foo 1994 08:49:37 GMT"), None);
180 }
181}