#[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use { crate::{ month::Month, weekday::Weekday, year::Year, zone::TimeZone, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, }, std::{ cmp, fmt, num::TryFromIntError, time::{SystemTime, SystemTimeError, UNIX_EPOCH}, }, }; #[derive(Debug)] pub enum Error { System(SystemTimeError), RangeError, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::System(e) => write!(f, "{e}"), Self::RangeError => write!(f, "self:?"), } } } impl From for Error { fn from(value: SystemTimeError) -> Self { Self::System(value) } } impl From for Error { fn from(_value: TryFromIntError) -> Self { Self::RangeError } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct DateTime { pub year: Year, pub month: Month, pub day: u8, pub hour: u8, pub minute: u8, pub second: u8, pub zone: TimeZone, } impl DateTime { /// Retrieves the year pub fn year(&self) -> i32 { self.year.get() } fn timestamp_naive(&self) -> i64 { let mut seconds: i64 = 0; let mut year = Year::new(1970); if self.year() < 1970 { while year.get() > self.year() { year = year.previous(); seconds -= year.seconds(); } } else if self.year() > 1970 { while year.get() < self.year() { seconds += year.seconds(); year = year.next(); } } let mut month = Month::Janurary; while month < self.month { seconds += month.seconds(self.year); month = month.next().unwrap(); } // The days begin numbering with 1, so on the 5th we have had four full // days plus some remainder. So we use self.days - 1 for our calculations seconds += i64::from(self.day - 1) * SECONDS_PER_DAY; seconds += i64::from(self.hour) * SECONDS_PER_HOUR; seconds += i64::from(self.minute) * SECONDS_PER_MINUTE; seconds += i64::from(self.second); seconds } /// Gets the Unix timestamp corresponding to this `DateTime` pub fn timestamp(&self) -> i64 { let mut seconds = self.timestamp_naive(); seconds += self.zone.as_seconds(); seconds } #[allow(clippy::missing_panics_doc)] /// Converts a Unix timestamp to a `DateTime` struct pub fn from_timestamp(ts: i64) -> Self { if ts < 0 { let mut seconds: i64 = ts; let mut year = Year::new(1969); while seconds < -year.seconds() { seconds += year.seconds(); year = year.previous(); } let mut month = Some(Month::December); while month.is_some() { let m = month.unwrap(); if -seconds < m.seconds(year) { break; } seconds += m.seconds(year); month = m.previous(); } let month = month.unwrap(); let mut day = month.days(year); while day > 0 && seconds < -SECONDS_PER_DAY { seconds += SECONDS_PER_DAY; day -= 1; } let mut hour: i8 = 23; while hour > 0 && seconds < -SECONDS_PER_HOUR { seconds += SECONDS_PER_HOUR; hour -= 1; } let hour = hour.try_into().unwrap(); let mut minute: i8 = 59; while minute > 0 && seconds < -60 { seconds += 60; minute -= 1; } let minute = minute.try_into().unwrap(); let second: u8 = (seconds + 60).try_into().unwrap(); Self { year, month, day, hour, minute, second, zone: TimeZone::Utc, } } else if ts > 0 { let mut seconds: i64 = ts; let mut year = Year::new(1970); while year.seconds() < seconds { seconds -= year.seconds(); year = year.next(); } let mut month = Month::Janurary; while month.seconds(year) < seconds { seconds -= month.seconds(year); month = month.next().unwrap(); } let day = seconds / SECONDS_PER_DAY + 1; seconds %= SECONDS_PER_DAY; let hour = seconds / SECONDS_PER_HOUR; seconds %= SECONDS_PER_HOUR; let minute = seconds / SECONDS_PER_MINUTE; seconds %= SECONDS_PER_MINUTE; Self { year, month, day: day.try_into().unwrap(), hour: hour.try_into().unwrap(), minute: minute.try_into().unwrap(), second: seconds.try_into().unwrap(), zone: TimeZone::Utc, } } else { Self { year: Year::new(1970), month: Month::Janurary, day: 1, hour: 0, minute: 0, second: 0, zone: TimeZone::Utc, } } } #[allow(clippy::missing_panics_doc)] /// Creates a `DateTime` from the system time pub fn now() -> Result { let now = SystemTime::now().duration_since(UNIX_EPOCH)?; Ok(Self::from_timestamp(i64::try_from(now.as_secs())?)) } #[allow(clippy::missing_panics_doc)] /// Gets the day of the week for this `DateTime` pub fn weekday(&self) -> Weekday { let ts = self.timestamp_naive(); let mut days = ts / SECONDS_PER_DAY; // For negative timestamps this number is actually the following day if ts < 0 { days -= 1; } // Rusts `%` operator is modulo, not modulus. We have to use the // operation which will gove the correct answer for either positive // or negative remainders let rem = days.rem_euclid(7); rem.try_into().unwrap() } /// Returns a string representing this date and time in human readable format pub fn display(&self) -> String { format!( "{} {} {} {:0>2}:{:0>2}:{:0>2} {} {}", self.weekday().abbreviate(), self.month.abbreviate(), self.day, self.hour, self.minute, self.second, self.zone.display(), self.year(), ) } } impl fmt::Display for DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}{}", self.year.get(), self.month as u8, self.day, self.hour, self.minute, self.second, self.zone, ) } } impl Ord for DateTime { fn cmp(&self, other: &Self) -> cmp::Ordering { let a = self.timestamp(); let b = other.timestamp(); a.cmp(&b) } } impl PartialOrd for DateTime { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } #[cfg(test)] mod tests { use super::*; #[test] fn weekday() { let mut dt = DateTime { year: Year::new(2024), month: Month::Janurary, day: 29, hour: 18, minute: 14, second: 42, zone: TimeZone::Utc, }; assert_eq!(dt.weekday(), Weekday::Monday); dt = DateTime { year: Year::new(2024), month: Month::Janurary, day: 27, hour: 12, minute: 42, second: 14, zone: TimeZone::Offset { sign: crate::zone::Sign::Positive, hours: 3, minutes: 15, }, }; assert_eq!(dt.weekday(), Weekday::Saturday); dt = DateTime { year: Year::new(1965), month: Month::February, day: 9, hour: 4, minute: 4, second: 37, zone: TimeZone::Utc, }; assert_eq!(dt.weekday(), Weekday::Tuesday); dt.day -= 1; assert_eq!(dt.weekday(), Weekday::Monday); dt.day -= 1; assert_eq!(dt.weekday(), Weekday::Sunday); dt.day -= 1; assert_eq!(dt.weekday(), Weekday::Saturday); dt.day -= 1; assert_eq!(dt.weekday(), Weekday::Friday); dt.day -= 1; assert_eq!(dt.weekday(), Weekday::Thursday); dt.day -= 1; assert_eq!(dt.weekday(), Weekday::Wednesday); } #[test] fn ord() { let a = DateTime { year: Year::new(2024), month: Month::Janurary, day: 29, hour: 18, minute: 28, second: 42, zone: TimeZone::new(Some(4), None).unwrap(), }; let b = DateTime { year: Year::new(2024), month: Month::Janurary, day: 29, hour: 18, minute: 05, second: 42, zone: TimeZone::new(Some(5), None).unwrap(), }; assert!(a < b); } #[test] fn ts() { let ts: i64 = 1706571482; let dt = DateTime { year: Year::new(2024), month: Month::Janurary, day: 29, hour: 23, minute: 38, second: 02, zone: TimeZone::Utc, }; assert_eq!(dt.timestamp(), ts); assert_eq!(dt, DateTime::from_timestamp(ts)); } #[test] fn ts_negative() { let ts: i64 = -154382123; let dt = DateTime { year: Year::new(1965), month: Month::February, day: 9, hour: 4, minute: 4, second: 37, zone: TimeZone::Utc, }; assert_eq!(dt.timestamp(), ts); assert_eq!(dt, DateTime::from_timestamp(ts)); } #[test] fn fmt() { let mut dt = DateTime::from_timestamp(1706571482); assert_eq!(dt.to_string(), "2024-01-29T23:38:02Z"); dt.zone = TimeZone::Offset { sign: crate::zone::Sign::Positive, hours: 4, minutes: 15, }; assert_eq!(dt.to_string(), "2024-01-29T23:38:02+04:15") } #[test] fn display() { let mut dt = DateTime::from_timestamp(1706571482); assert_eq!(dt.display(), "Mon Jan 29 23:38:02 UTC 2024"); dt.zone = TimeZone::Offset { sign: crate::zone::Sign::Positive, hours: 4, minutes: 15, }; assert_eq!(dt.display(), "Mon Jan 29 23:38:02 +04:15 2024"); } }