use crate::{
bounds::RangeError,
civil::{Date, Time},
macros::{ctry, unwrapr},
tz::Offset,
Timestamp,
};
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct DateTime {
date: Date,
time: Time,
}
impl DateTime {
pub const MIN: DateTime = DateTime { date: Date::MIN, time: Time::MIN };
pub const MAX: DateTime = DateTime { date: Date::MAX, time: Time::MAX };
#[inline]
pub const fn new(
year: i16,
month: i8,
day: i8,
hour: i8,
minute: i8,
second: i8,
subsec_nanosecond: i32,
) -> Result<DateTime, RangeError> {
let date = ctry!(Date::new(year, month, day));
let time = ctry!(Time::new(hour, minute, second, subsec_nanosecond));
Ok(DateTime::from_parts(date, time))
}
#[inline]
pub const fn from_parts(date: Date, time: Time) -> DateTime {
DateTime { date, time }
}
#[inline]
pub const fn date(&self) -> Date {
self.date
}
#[inline]
pub const fn time(&self) -> Time {
self.time
}
#[inline]
pub const fn checked_add_seconds(
&self,
seconds: i32,
) -> Result<DateTime, RangeError> {
let (second, added_days) =
ctry!(self.time().to_second().overflowing_add(seconds));
let date = ctry!(self.date().checked_add(added_days));
let time = unwrapr!(
second
.to_time()
.with_subsec_nanosecond(self.time().subsec_nanosecond()),
"subsec we started from hasn't change and must be valid",
);
Ok(DateTime::from_parts(date, time))
}
#[inline]
pub const fn saturating_add_seconds(&self, seconds: i32) -> DateTime {
match self.checked_add_seconds(seconds) {
Ok(dt) => dt,
Err(_) => {
if seconds < 0 {
DateTime::MIN
} else {
DateTime::MAX
}
}
}
}
#[inline]
pub const fn to_timestamp(
&self,
offset: Offset,
) -> Result<Timestamp, RangeError> {
offset.to_timestamp(*self)
}
}
impl core::fmt::Debug for DateTime {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "{:?}T{:?}", self.date(), self.time())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn datetime(
year: i16,
month: i8,
day: i8,
hour: i8,
minute: i8,
second: i8,
subsec_nanosecond: i32,
) -> DateTime {
DateTime::new(
year,
month,
day,
hour,
minute,
second,
subsec_nanosecond,
)
.unwrap()
}
fn stamp(second: i64, subsec: i32) -> Timestamp {
Timestamp::new(second, subsec).unwrap()
}
fn offset(second: i32) -> Offset {
Offset::from_seconds(second).unwrap()
}
#[test]
fn checked_add_seconds() {
let dt = datetime(2026, 2, 25, 0, 0, 0, 0);
assert_eq!(
dt.checked_add_seconds(1),
Ok(datetime(2026, 2, 25, 0, 0, 1, 0))
);
assert_eq!(
dt.checked_add_seconds(86_399),
Ok(datetime(2026, 2, 25, 23, 59, 59, 0))
);
assert_eq!(
dt.checked_add_seconds(86_400),
Ok(datetime(2026, 2, 26, 0, 0, 0, 0))
);
assert_eq!(
dt.checked_add_seconds(-1),
Ok(datetime(2026, 2, 24, 23, 59, 59, 0))
);
assert_eq!(
dt.checked_add_seconds(-86_399),
Ok(datetime(2026, 2, 24, 0, 0, 1, 0))
);
assert_eq!(
dt.checked_add_seconds(-86_400),
Ok(datetime(2026, 2, 24, 0, 0, 0, 0))
);
let dt = datetime(2026, 2, 25, 0, 0, 0, 1);
assert_eq!(
dt.checked_add_seconds(1),
Ok(datetime(2026, 2, 25, 0, 0, 1, 1))
);
assert_eq!(
dt.checked_add_seconds(86_399),
Ok(datetime(2026, 2, 25, 23, 59, 59, 1))
);
assert_eq!(
dt.checked_add_seconds(86_400),
Ok(datetime(2026, 2, 26, 0, 0, 0, 1))
);
assert_eq!(
dt.checked_add_seconds(-1),
Ok(datetime(2026, 2, 24, 23, 59, 59, 1))
);
assert_eq!(
dt.checked_add_seconds(-86_399),
Ok(datetime(2026, 2, 24, 0, 0, 1, 1))
);
assert_eq!(
dt.checked_add_seconds(-86_400),
Ok(datetime(2026, 2, 24, 0, 0, 0, 1))
);
}
#[test]
fn to_timestamp_no_subsec() {
let dt = datetime(1970, 1, 1, 0, 0, 0, 0);
assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(0, 0)));
assert_eq!(dt.to_timestamp(offset(3600)), Ok(stamp(-3600, 0)));
assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(3600, 0)));
let dt = datetime(1969, 12, 31, 23, 30, 0, 0);
assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(-1800, 0)));
assert_eq!(dt.to_timestamp(offset(3600)), Ok(stamp(-5400, 0)));
assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(1800, 0)));
let dt = datetime(1970, 1, 1, 0, 30, 0, 0);
assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(1800, 0)));
assert_eq!(dt.to_timestamp(offset(3600)), Ok(stamp(-1800, 0)));
assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(5400, 0)));
}
#[test]
fn to_timestamp_with_subsec() {
let dt = datetime(1970, 1, 1, 0, 0, 0, 123);
assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(0, 123)));
assert_eq!(
dt.to_timestamp(offset(3600)),
Ok(stamp(-3599, -999_999_877))
);
assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(3600, 123)));
let dt = datetime(1969, 12, 31, 23, 30, 0, 123);
assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(-1799, -999_999_877)));
assert_eq!(
dt.to_timestamp(offset(3600)),
Ok(stamp(-5399, -999_999_877))
);
assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(1800, 123)));
let dt = datetime(1970, 1, 1, 0, 30, 0, 123);
assert_eq!(dt.to_timestamp(offset(0)), Ok(stamp(1800, 123)));
assert_eq!(
dt.to_timestamp(offset(3600)),
Ok(stamp(-1799, -999_999_877))
);
assert_eq!(dt.to_timestamp(offset(-3600)), Ok(stamp(5400, 123)));
}
#[test]
fn to_timestamp_err() {
let dt = datetime(-9999, 1, 1, 0, 0, 0, 0);
assert_eq!(dt.to_timestamp(Offset::MIN), Ok(Timestamp::MIN));
assert!(dt.to_timestamp(offset(Offset::MIN.seconds() + 1)).is_err());
let dt = datetime(9999, 12, 31, 23, 59, 59, 999_999_999);
assert_eq!(dt.to_timestamp(Offset::MAX), Ok(Timestamp::MAX));
assert!(dt.to_timestamp(offset(Offset::MAX.seconds() - 1)).is_err());
let dt = datetime(9999, 12, 31, 23, 59, 59, 0);
assert!(dt.to_timestamp(offset(Offset::MAX.seconds() - 1)).is_err());
}
}