epoch-rs/src/datetime.rs

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");
}
}