use crate::constants::{HOURS_IN_DAY, MINUTES_IN_HOUR, SECONDS_IN_DAY, SECONDS_IN_MINUTE};
use crate::date::Date;
use crate::date_error::DateError;
use crate::date_error::DateErrorKind;
use crate::time::Time;
use crate::utils::crossplatform_util;
use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
#[derive(Copy, Clone)]
#[cfg_attr(feature = "serde-struct", derive(serde::Serialize, serde::Deserialize))]
pub struct DateTime {
pub date: Date,
pub time: Time,
pub shift_minutes: isize,
}
#[cfg(all(feature = "serde", not(feature = "serde-struct")))]
impl serde::Serialize for DateTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let seconds = self.to_seconds_from_unix_epoch_gmt();
serializer.serialize_u64(seconds)
}
}
#[cfg(all(feature = "serde", not(feature = "serde-struct")))]
impl<'de> serde::Deserialize<'de> for DateTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let seconds = u64::deserialize(deserializer)?;
let dt = DateTime::from_seconds_since_unix_epoch(seconds);
Ok(DateTime::new(dt.date, dt.time, 0))
}
}
impl DateTime {
pub fn new(date: Date, time: Time, shift_minutes: isize) -> Self {
DateTime {
date,
time,
shift_minutes,
}
}
pub fn to_iso_8061(&self) -> String {
format!("{}T{}{}", self.date, self.time, self.shift_string())
}
pub fn shift_string(&self) -> String {
if self.shift_minutes == 0 {
return "Z".to_string();
}
let hours = (self.shift_minutes.abs() / 60) as u64;
let minutes = (self.shift_minutes.abs() % 60) as u64;
if self.shift_minutes.is_positive() {
format!("+{:02}:{:02}", hours, minutes)
} else {
format!("-{:02}:{:02}", hours, minutes)
}
}
pub fn from_seconds_since_unix_epoch(seconds: u64) -> Self {
let (date, seconds) = Date::from_seconds_since_unix_epoch(seconds);
let time = Time::from_seconds(seconds);
DateTime::new(date, time, 0)
}
pub fn to_seconds_from_unix_epoch(&self) -> u64 {
self.date.to_seconds_from_unix_epoch(false) + self.time.to_seconds()
}
pub fn to_seconds_from_unix_epoch_gmt(&self) -> u64 {
(self.to_seconds_from_unix_epoch() as i128
- self.shift_minutes as i128 * SECONDS_IN_MINUTE as i128) as u64
}
pub fn now() -> Self {
Self::from_seconds_since_unix_epoch(crossplatform_util::now_seconds())
}
pub fn now_seconds() -> u64 {
crossplatform_util::now_seconds()
}
pub fn now_milliseconds() -> u128 {
crossplatform_util::now_milliseconds()
}
pub fn set_shift(&mut self, minutes: isize) {
if minutes > self.shift_minutes {
*self = self.add_seconds((minutes - self.shift_minutes) as u64 * SECONDS_IN_MINUTE)
} else {
*self = self.sub_seconds((self.shift_minutes - minutes) as u64 * SECONDS_IN_MINUTE)
}
self.shift_minutes = minutes;
}
pub fn add_seconds(&self, seconds: u64) -> Self {
let total_seconds = self.time.to_seconds() + seconds;
Self::new(
self.date.add_days(total_seconds / SECONDS_IN_DAY),
Time::from_seconds(total_seconds % SECONDS_IN_DAY),
self.shift_minutes,
)
}
pub fn add_time(&self, time: Time) -> Self {
Self::new(self.date, self.time + time, self.shift_minutes).normalize()
}
pub fn sub_seconds(&mut self, seconds: u64) -> Self {
let mut days = seconds / SECONDS_IN_DAY;
let seconds = seconds % SECONDS_IN_DAY;
let time_seconds = self.time.to_seconds();
let seconds = if time_seconds < seconds {
days += 1;
SECONDS_IN_DAY - seconds + time_seconds
} else {
time_seconds - seconds
};
Self::new(
self.date.sub_days(days),
Time::from_seconds(seconds),
self.shift_minutes,
)
}
fn shift_from_str(shift_str: &str) -> Result<isize, DateError> {
if shift_str.len() == 0 || &shift_str[0..1] == "Z" {
return Ok(0);
}
let mut split = (&shift_str[1..]).split(":");
let err = || DateErrorKind::WrongTimeShiftStringFormat;
let hour: u64 = (split.next().ok_or(err())?).parse().or(Err(err()))?;
let minute: u64 = (split.next().ok_or(err())?).parse().or(Err(err()))?;
let mut minutes: isize = (hour * MINUTES_IN_HOUR + minute) as isize;
if &shift_str[0..1] == "-" {
minutes = 0 - minutes;
}
Ok(minutes)
}
pub fn normalize(&self) -> DateTime {
let date = self.date.normalize();
let mut time = self.time.normalize();
let days = time.hour / 24;
time.hour %= 24;
Self::new(date.add_days(days), time, self.shift_minutes)
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.date, self.time)
}
}
impl fmt::Debug for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl Ord for DateTime {
fn cmp(&self, other: &Self) -> Ordering {
match self.date.cmp(&other.date) {
Ordering::Equal if self.shift_minutes == other.shift_minutes => {
self.time.cmp(&other.time)
}
Ordering::Equal => {
let lhs = self.time.to_minutes() as i64 - self.shift_minutes as i64;
let rhs = other.time.to_minutes() as i64 - other.shift_minutes as i64;
match lhs.cmp(&rhs) {
Ordering::Equal => (self.time.second + self.time.microsecond)
.cmp(&(other.time.second + other.time.microsecond)),
ordering => ordering,
}
}
ordering => ordering,
}
}
}
impl FromStr for DateTime {
type Err = DateError;
fn from_str(date_time_str: &str) -> Result<Self, Self::Err> {
let bytes = date_time_str.as_bytes();
let len = bytes.len();
if len < 11 || bytes[10] != b'T' {
return Err(DateErrorKind::WrongDateTimeStringFormat.into());
}
let date: Date = std::str::from_utf8(&bytes[0..10])
.map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
.parse()?;
if len <= 19 {
return Err(DateErrorKind::WrongDateTimeStringFormat.into());
}
for i in 19..len {
match bytes[i] {
b'Z' | b'+' | b'-' => {
return Ok(DateTime::new(
date,
std::str::from_utf8(&bytes[11..i])
.map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?
.parse()?,
DateTime::shift_from_str(
std::str::from_utf8(&bytes[i..])
.map_err(|_| DateErrorKind::WrongDateTimeStringFormat)?,
)?,
));
}
_ => {}
}
}
Err(DateErrorKind::WrongDateTimeStringFormat.into())
}
}
impl PartialOrd for DateTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for DateTime {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for DateTime {}
impl std::ops::Sub for DateTime {
type Output = Time;
fn sub(self, rhs: Self) -> Self::Output {
self.time + Time::from_hours((self.date - rhs.date) * HOURS_IN_DAY) - rhs.time
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::MINUTES_IN_HOUR;
#[test]
fn test_from_seconds_since_unix_epoch() {
let date_time = DateTime::new(Date::new(2021, 4, 13), Time::new(20, 55, 50), 0);
assert_eq!(
DateTime::from_seconds_since_unix_epoch(1618347350),
date_time
);
assert_eq!(date_time.to_seconds_from_unix_epoch(), 1618347350);
}
#[test]
fn test_date_time_cmp() {
let mut lhs = DateTime::new(Date::new(2019, 12, 31), Time::new(12, 0, 0), 0);
let mut rhs = lhs;
assert_eq!(lhs, rhs);
rhs.time.hour += 1;
assert!(lhs < rhs);
lhs.shift_minutes = -60;
assert_eq!(lhs, rhs);
}
#[test]
fn test_date_time_to_string() {
let date_time = DateTime::new(
Date::new(2021, 7, 28),
Time::new(10, 0, 0),
-4 * MINUTES_IN_HOUR as isize,
);
assert_eq!(date_time.to_iso_8061(), "2021-07-28T10:00:00-04:00");
assert_eq!(date_time.to_string(), "2021-07-28 10:00:00");
}
#[test]
fn test_shift_from_str() -> Result<(), DateError> {
assert_eq!(DateTime::shift_from_str("+4:30")?, 270);
assert_eq!(DateTime::shift_from_str("-4:30")?, -270);
assert_eq!(DateTime::shift_from_str("Z")?, 0);
Ok(())
}
#[test]
fn test_date_time_from_str() -> Result<(), DateError> {
assert_eq!(
"2021-07-28T10:00:00-4:00".parse::<DateTime>()?,
DateTime::new(
Date::new(2021, 7, 28),
Time::new(10, 0, 0),
-4 * MINUTES_IN_HOUR as isize
)
);
assert_eq!(
"2021-07-28T10:00:00+02:00".parse::<DateTime>()?,
DateTime::new(
Date::new(2021, 7, 28),
Time::new(10, 0, 0),
2 * MINUTES_IN_HOUR as isize
)
);
assert_eq!(
"2021-07-28T10:00:00Z".parse::<DateTime>()?,
DateTime::new(Date::new(2021, 7, 28), Time::new(10, 0, 0), 0)
);
assert_eq!(
"2020-01-09T21:10:05.779325Z".parse::<DateTime>()?,
DateTime::new(
Date::new(2020, 1, 9),
Time::new_with_microseconds(21, 10, 5, 779325),
0
)
);
Ok(())
}
#[test]
fn test_to_seconds_since_unix_epoch_gmt() {
let date_time = DateTime::new(
Date::new(2023, 1, 13),
Time::new(8, 40, 42),
-5 * MINUTES_IN_HOUR as isize,
);
assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
let date_time = DateTime::new(
Date::new(2023, 1, 13),
Time::new(14, 40, 42),
1 * MINUTES_IN_HOUR as isize,
);
assert_eq!(1673617242, date_time.to_seconds_from_unix_epoch_gmt());
}
#[test]
fn test_date_time_normalize() {
let date_time = DateTime::new(
Date::new(2023, 1, 13),
Time::new(24, 0, 42),
-5 * MINUTES_IN_HOUR as isize,
);
let date_time2 = DateTime::new(
Date::new(2023, 1, 14),
Time::new(0, 0, 42),
-5 * MINUTES_IN_HOUR as isize,
);
assert_eq!(date_time.normalize(), date_time2);
}
#[test]
fn test_date_time_from_str_invalid() {
assert!("invalid".parse::<DateTime>().is_err());
assert!("2020-01-01".parse::<DateTime>().is_err());
assert!("2020-01-01T".parse::<DateTime>().is_err());
assert!("2020-01-01T12:00:00".parse::<DateTime>().is_err());
}
#[test]
fn test_shift_string_formatting() {
let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
assert_eq!(dt_utc.shift_string(), "Z");
let dt_plus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 120);
assert_eq!(dt_plus.shift_string(), "+02:00");
let dt_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), -300);
assert_eq!(dt_minus.shift_string(), "-05:00");
let dt_plus_30 = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 30);
assert_eq!(dt_plus_30.shift_string(), "+00:30");
}
#[test]
fn test_iso_8601_formatting() {
let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 0);
assert_eq!(dt.to_iso_8061(), "2020-01-01T12:30:45Z");
let dt_tz = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), 120);
assert_eq!(dt_tz.to_iso_8061(), "2020-01-01T12:30:45+02:00");
let dt_tz_minus = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 30, 45), -300);
assert_eq!(dt_tz_minus.to_iso_8061(), "2020-01-01T12:30:45-05:00");
}
#[test]
fn test_date_time_arithmetic() {
let dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
let dt_plus_sec = dt.add_seconds(3600); assert_eq!(dt_plus_sec.time.hour, 13);
assert_eq!(dt_plus_sec.date, Date::new(2020, 1, 1));
let dt_plus_day = dt.add_seconds(86400); assert_eq!(dt_plus_day.date, Date::new(2020, 1, 2));
assert_eq!(dt_plus_day.time, Time::new(12, 0, 0));
let time_to_add = Time::new(2, 30, 0);
let dt_plus_time = dt.add_time(time_to_add);
assert_eq!(dt_plus_time.time, Time::new(14, 30, 0));
}
#[test]
fn test_date_time_subtraction() {
let dt1 = DateTime::new(Date::new(2020, 1, 2), Time::new(12, 0, 0), 0);
let dt2 = DateTime::new(Date::new(2020, 1, 1), Time::new(10, 0, 0), 0);
let diff = dt1 - dt2;
assert_eq!(diff, Time::new(26, 0, 0)); }
#[test]
fn test_timezone_shift_operations() {
let mut dt = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
dt.set_shift(120); assert_eq!(dt.shift_minutes, 120);
assert_eq!(dt.time, Time::new(14, 0, 0));
dt.set_shift(-300); assert_eq!(dt.shift_minutes, -300);
assert_eq!(dt.time, Time::new(7, 0, 0)); }
#[test]
fn test_unix_epoch_conversions() {
let dt = DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0);
assert_eq!(dt.to_seconds_from_unix_epoch(), 0);
let dt_from_epoch = DateTime::from_seconds_since_unix_epoch(0);
assert_eq!(dt_from_epoch.date, Date::new(1970, 1, 1));
assert_eq!(dt_from_epoch.time, Time::new(0, 0, 0));
let dt_tz = DateTime::new(Date::new(1970, 1, 1), Time::new(12, 0, 0), 0);
let gmt_seconds = dt_tz.to_seconds_from_unix_epoch_gmt();
assert_eq!(gmt_seconds, 43200); }
#[test]
fn test_date_time_comparison_with_timezone() {
let dt_utc = DateTime::new(Date::new(2020, 1, 1), Time::new(12, 0, 0), 0);
let dt_est = DateTime::new(Date::new(2020, 1, 1), Time::new(7, 0, 0), -300);
assert_eq!(dt_utc, dt_est);
let dt_different = DateTime::new(Date::new(2020, 1, 1), Time::new(8, 0, 0), -300);
assert_ne!(dt_utc, dt_different);
}
#[test]
fn test_edge_cases() {
let dt_leap = DateTime::new(Date::new(2020, 2, 29), Time::new(12, 0, 0), 0);
assert!(dt_leap.date.valid());
let dt_year_end = DateTime::new(Date::new(2020, 12, 31), Time::new(23, 59, 59), 0);
let dt_next_year = dt_year_end.add_seconds(1);
assert_eq!(dt_next_year.date, Date::new(2021, 1, 1));
assert_eq!(dt_next_year.time, Time::new(0, 0, 0));
}
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
use serde_json;
#[test]
#[cfg(not(feature = "serde-struct"))]
fn test_serde_unix_epoch() {
let dt = DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0);
let json = serde_json::to_string(&dt).unwrap();
assert_eq!(json, "0");
let deserialized: DateTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.date, dt.date);
assert_eq!(deserialized.time, dt.time);
assert_eq!(deserialized.shift_minutes, 0);
let dt = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 30, 45), 0);
let expected_seconds = dt.to_seconds_from_unix_epoch_gmt();
let json = serde_json::to_string(&dt).unwrap();
assert_eq!(json, expected_seconds.to_string());
let deserialized: DateTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.date, dt.date);
assert_eq!(deserialized.time, dt.time);
assert_eq!(deserialized.shift_minutes, 0);
}
#[test]
#[cfg(not(feature = "serde-struct"))]
fn test_serde_unix_epoch_with_timezone() {
let dt = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), -300); let expected_utc_seconds = dt.to_seconds_from_unix_epoch_gmt();
let json = serde_json::to_string(&dt).unwrap();
assert_eq!(json, expected_utc_seconds.to_string());
let deserialized: DateTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.shift_minutes, 0);
assert_eq!(deserialized.to_seconds_from_unix_epoch_gmt(), expected_utc_seconds);
}
#[test]
#[cfg(feature = "serde-struct")]
fn test_serde_struct() {
let dt = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 30, 45), 0);
let json = serde_json::to_string(&dt).unwrap();
assert!(json.contains("\"date\""));
assert!(json.contains("\"time\""));
assert!(json.contains("\"shift_minutes\":0"));
let deserialized: DateTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, dt);
let dt = DateTime::new(Date::new(2020, 2, 29), Time::new(23, 59, 59), -300);
let json = serde_json::to_string(&dt).unwrap();
assert!(json.contains("\"shift_minutes\":-300"));
let deserialized: DateTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, dt);
}
#[test]
fn test_serde_roundtrip() {
let datetimes = vec![
DateTime::new(Date::new(1970, 1, 1), Time::new(0, 0, 0), 0),
DateTime::new(Date::new(2024, 1, 15), Time::new(12, 30, 45), 0),
DateTime::new(Date::new(2020, 2, 29), Time::new(23, 59, 59), 0),
DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), -300),
DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), 120),
];
for dt in datetimes {
let json = serde_json::to_string(&dt).unwrap();
let deserialized: DateTime = serde_json::from_str(&json).unwrap();
#[cfg(not(feature = "serde-struct"))]
{
assert_eq!(deserialized.shift_minutes, 0);
assert_eq!(
deserialized.to_seconds_from_unix_epoch_gmt(),
dt.to_seconds_from_unix_epoch_gmt()
);
}
#[cfg(feature = "serde-struct")]
{
assert_eq!(deserialized, dt, "Failed roundtrip for datetime: {}", dt);
}
}
}
#[test]
#[cfg(not(feature = "serde-struct"))]
fn test_serde_utc_preservation() {
let dt_utc = DateTime::new(Date::new(2024, 1, 15), Time::new(12, 0, 0), 0);
let json = serde_json::to_string(&dt_utc).unwrap();
let deserialized: DateTime = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, dt_utc);
}
}
}