385 lines
11 KiB
Rust
385 lines
11 KiB
Rust
#[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<SystemTimeError> for Error {
|
|
fn from(value: SystemTimeError) -> Self {
|
|
Self::System(value)
|
|
}
|
|
}
|
|
|
|
impl From<TryFromIntError> 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<Self, Error> {
|
|
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<cmp::Ordering> {
|
|
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");
|
|
}
|
|
}
|