use icu_calendar::{types::RataDie, AsCalendar, Date, Iso, RangeError};
use crate::zone::UtcOffset;
macro_rules! dt_unit {
($name:ident, $storage:ident, $value:expr, $(#[$docs:meta])+) => {
$(#[$docs])+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct $name(pub(crate) $storage);
impl $name {
pub const fn number(self) -> $storage {
self.0
}
pub const fn zero() -> $name {
Self(0)
}
#[inline]
pub fn is_zero(self) -> bool {
self.0 == 0
}
}
impl TryFrom<$storage> for $name {
type Error = RangeError;
fn try_from(input: $storage) -> Result<Self, Self::Error> {
if input > $value {
Err(RangeError {
field: stringify!($name),
min: 0,
max: $value,
value: input as i32,
})
} else {
Ok(Self(input))
}
}
}
impl TryFrom<usize> for $name {
type Error = RangeError;
fn try_from(input: usize) -> Result<Self, Self::Error> {
if input > $value {
Err(RangeError {
field: "$name",
min: 0,
max: $value,
value: input as i32,
})
} else {
Ok(Self(input as $storage))
}
}
}
impl From<$name> for $storage {
fn from(input: $name) -> Self {
input.0
}
}
impl From<$name> for usize {
fn from(input: $name) -> Self {
input.0 as Self
}
}
};
}
dt_unit!(
Hour,
u8,
23,
);
dt_unit!(
Minute,
u8,
59,
);
dt_unit!(
Second,
u8,
60,
);
dt_unit!(
Nanosecond,
u32,
999_999_999,
);
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[allow(clippy::exhaustive_structs)] pub struct Time {
pub hour: Hour,
pub minute: Minute,
pub second: Second,
pub subsecond: Nanosecond,
}
impl Time {
pub const fn new(hour: Hour, minute: Minute, second: Second, subsecond: Nanosecond) -> Self {
Self {
hour,
minute,
second,
subsecond,
}
}
pub const fn start_of_day() -> Self {
Self {
hour: Hour(0),
minute: Minute(0),
second: Second(0),
subsecond: Nanosecond(0),
}
}
pub const fn noon() -> Self {
Self {
hour: Hour(12),
minute: Minute(0),
second: Second(0),
subsecond: Nanosecond(0),
}
}
pub fn try_new(hour: u8, minute: u8, second: u8, nanosecond: u32) -> Result<Self, RangeError> {
Ok(Self {
hour: hour.try_into()?,
minute: minute.try_into()?,
second: second.try_into()?,
subsecond: nanosecond.try_into()?,
})
}
pub(crate) const fn seconds_since_midnight(self) -> u32 {
(self.hour.0 as u32 * 60 + self.minute.0 as u32) * 60 + self.second.0 as u32
}
}
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)] pub struct DateTime<A: AsCalendar> {
pub date: Date<A>,
pub time: Time,
}
impl<A, B> PartialEq<DateTime<B>> for DateTime<A>
where
A: AsCalendar,
B: AsCalendar,
Date<A>: PartialEq<Date<B>>,
{
fn eq(&self, other: &DateTime<B>) -> bool {
self.date.eq(&other.date) && self.time.eq(&other.time)
}
}
impl<A: AsCalendar> Eq for DateTime<A> where Date<A>: Eq {}
impl<A: AsCalendar> Clone for DateTime<A>
where
Date<A>: Clone,
{
fn clone(&self) -> Self {
Self {
date: self.date.clone(),
time: self.time,
}
}
}
impl<A: AsCalendar> Copy for DateTime<A> where Date<A>: Copy {}
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)] pub struct ZonedDateTime<A: AsCalendar, Z> {
pub date: Date<A>,
pub time: Time,
pub zone: Z,
}
impl<A, B, Y, Z> PartialEq<ZonedDateTime<B, Z>> for ZonedDateTime<A, Y>
where
A: AsCalendar,
B: AsCalendar,
Date<A>: PartialEq<Date<B>>,
Y: PartialEq<Z>,
{
fn eq(&self, other: &ZonedDateTime<B, Z>) -> bool {
self.date.eq(&other.date) && self.time.eq(&other.time) && self.zone.eq(&other.zone)
}
}
impl<A: AsCalendar, Z> Eq for ZonedDateTime<A, Z>
where
Date<A>: Eq,
Z: Eq,
{
}
impl<A: AsCalendar, Z> Clone for ZonedDateTime<A, Z>
where
Date<A>: Clone,
Z: Clone,
{
fn clone(&self) -> Self {
Self {
date: self.date.clone(),
time: self.time,
zone: self.zone.clone(),
}
}
}
impl<A: AsCalendar, Z> Copy for ZonedDateTime<A, Z>
where
Date<A>: Copy,
Z: Copy,
{
}
const UNIX_EPOCH: RataDie = calendrical_calculations::gregorian::fixed_from_gregorian(1970, 1, 1);
impl<A: AsCalendar> Ord for ZonedDateTime<A, UtcOffset> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
let mut srd = self.date.to_rata_die();
let mut ord = other.date.to_rata_die();
if srd + 3 <= ord {
return core::cmp::Ordering::Less;
}
if srd - 3 >= ord {
return core::cmp::Ordering::Greater;
}
let mut ss = self.time.seconds_since_midnight() as i32 - self.zone.to_seconds();
let mut os = other.time.seconds_since_midnight() as i32 - other.zone.to_seconds();
if ss < 0 {
srd -= 1;
ss += 24 * 60 * 60;
}
if ss > 24 * 60 * 60 {
srd += 1;
ss -= 24 * 60 * 60;
}
if os < 0 {
ord -= 1;
os += 24 * 60 * 60;
}
if os > 24 * 60 * 60 {
ord += 1;
os -= 24 * 60 * 60;
}
srd.cmp(&ord)
.then(ss.cmp(&os))
.then(self.time.subsecond.cmp(&other.time.subsecond))
}
}
impl<A> PartialOrd<ZonedDateTime<A, UtcOffset>> for ZonedDateTime<A, UtcOffset>
where
A: AsCalendar,
Date<A>: PartialEq<Date<A>>,
{
fn partial_cmp(&self, other: &ZonedDateTime<A, UtcOffset>) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl ZonedDateTime<Iso, UtcOffset> {
pub fn from_epoch_milliseconds_and_utc_offset(
epoch_milliseconds: i64,
utc_offset: UtcOffset,
) -> Self {
let (utc_epoch_days, utc_time_millisecs) = (
epoch_milliseconds.div_euclid(86400000),
epoch_milliseconds.rem_euclid(86400000),
);
let offset_millisecs = 1000 * (utc_offset.to_seconds() as i64);
let local_time_millisecs = utc_time_millisecs + offset_millisecs;
let day_adjustment = local_time_millisecs.div_euclid(86400000);
let final_time_millisecs = local_time_millisecs.rem_euclid(86400000);
let rata_die = UNIX_EPOCH + utc_epoch_days + day_adjustment;
#[expect(clippy::unwrap_used)] let time = Time::try_new(
(final_time_millisecs / 3600000) as u8,
((final_time_millisecs % 3600000) / 60000) as u8,
((final_time_millisecs % 60000) / 1000) as u8,
((final_time_millisecs % 1000) as u32) * 1000000,
)
.unwrap();
ZonedDateTime {
date: Date::from_rata_die(rata_die, Iso),
time,
zone: utc_offset,
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[allow(clippy::exhaustive_structs)] #[cfg(feature = "unstable")]
pub struct ZonedTime<Z> {
pub time: Time,
pub zone: Z,
}