use ::chrono::{
DateTime as ChronoDateTime, Datelike, NaiveDate, NaiveTime, Timelike, Utc, Weekday,
};
use crate::{Date, DateTime, DayName, HttpDate, Time};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChronoError(String);
impl ChronoError {
fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
impl std::fmt::Display for ChronoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ChronoError {}
fn posix_year(two_digit_year: u16) -> u16 {
if (69..=99).contains(&two_digit_year) {
1900 + two_digit_year
} else {
2000 + two_digit_year
}
}
fn dayname_from_weekday(weekday: Weekday) -> DayName {
match weekday {
Weekday::Mon => DayName::Mon,
Weekday::Tue => DayName::Tue,
Weekday::Wed => DayName::Wed,
Weekday::Thu => DayName::Thu,
Weekday::Fri => DayName::Fri,
Weekday::Sat => DayName::Sat,
Weekday::Sun => DayName::Sun,
}
}
impl TryFrom<HttpDate> for ChronoDateTime<Utc> {
type Error = ChronoError;
fn try_from(date: HttpDate) -> Result<Self, Self::Error> {
let dt = date.datetime();
let year = if date.is_rfc850() {
posix_year(dt.date.year())
} else {
dt.date.year()
};
let date =
NaiveDate::from_ymd_opt(year.into(), dt.date.month().into(), dt.date.day().into())
.ok_or_else(|| {
ChronoError::new(format!(
"{year:04}-{:02}-{:02} is not a valid calendar date",
dt.date.month(),
dt.date.day(),
))
})?;
let time = NaiveTime::from_hms_opt(
dt.time.hour().into(),
dt.time.minute().into(),
dt.time.second().into(),
)
.ok_or_else(|| ChronoError::new("time out of range"))?;
Ok(ChronoDateTime::from_naive_utc_and_offset(
date.and_time(time),
Utc,
))
}
}
#[allow(clippy::cast_possible_truncation)] impl TryFrom<ChronoDateTime<Utc>> for HttpDate {
type Error = ChronoError;
fn try_from(dt: ChronoDateTime<Utc>) -> Result<Self, Self::Error> {
let naive = dt.naive_utc();
let date = u16::try_from(naive.year())
.ok()
.and_then(|year| Date::new(year, naive.month() as u8, naive.day() as u8).ok())
.ok_or_else(|| ChronoError::new(format!("year {} is outside 0-9999", naive.year())))?;
let time = Time::new(
naive.hour() as u8,
naive.minute() as u8,
naive.second() as u8,
)
.map_err(|_| ChronoError::new("time out of range"))?;
Ok(HttpDate::imf_fixdate(DateTime {
dayname: dayname_from_weekday(dt.weekday()),
date,
time,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{decode, encode};
use ::chrono::TimeZone;
fn to_chrono(input: &str) -> ChronoDateTime<Utc> {
ChronoDateTime::<Utc>::try_from(decode(input).unwrap()).unwrap()
}
#[test]
fn imf_fixdate_to_chrono() {
let c = to_chrono("Sun, 06 Nov 1994 08:49:37 GMT");
assert_eq!(c, Utc.with_ymd_and_hms(1994, 11, 6, 8, 49, 37).unwrap());
}
#[test]
fn asctime_to_chrono() {
let c = to_chrono("Sun Nov 6 08:49:37 1994");
assert_eq!(c, Utc.with_ymd_and_hms(1994, 11, 6, 8, 49, 37).unwrap());
}
#[test]
fn rfc850_two_digit_year_uses_posix_window() {
assert_eq!(to_chrono("Sunday, 06-Nov-94 08:49:37 GMT").year(), 1994);
assert_eq!(to_chrono("Sunday, 06-Nov-69 08:49:37 GMT").year(), 1969);
assert_eq!(to_chrono("Sunday, 06-Nov-68 08:49:37 GMT").year(), 2068);
assert_eq!(to_chrono("Sunday, 06-Nov-00 08:49:37 GMT").year(), 2000);
}
#[test]
fn impossible_calendar_date_is_rejected() {
let date = decode("Sun, 31 Feb 1994 08:49:37 GMT").unwrap();
assert!(ChronoDateTime::<Utc>::try_from(date).is_err());
}
#[test]
fn chrono_to_http_date_is_imf_fixdate() {
let c = Utc.with_ymd_and_hms(1994, 11, 6, 8, 49, 37).unwrap();
let date = HttpDate::try_from(c).unwrap();
assert!(date.is_imf_fixdate());
assert_eq!(encode(&date), "Sun, 06 Nov 1994 08:49:37 GMT");
}
#[test]
fn chrono_year_outside_0_9999_is_rejected() {
let c = Utc.with_ymd_and_hms(10_000, 1, 1, 0, 0, 0).unwrap();
assert!(HttpDate::try_from(c).is_err());
}
#[test]
fn round_trip_via_chrono() {
let expected =
HttpDate::try_from(Utc.with_ymd_and_hms(1994, 11, 6, 8, 49, 37).unwrap()).unwrap();
for input in [
"Sun, 06 Nov 1994 08:49:37 GMT",
"Sunday, 06-Nov-94 08:49:37 GMT",
"Sun Nov 6 08:49:37 1994",
] {
let original = decode(input).unwrap();
let c = ChronoDateTime::<Utc>::try_from(original).unwrap();
let back = HttpDate::try_from(c).unwrap();
assert_eq!(back, expected, "round-trip of {input:?}");
}
}
}