use crate::error::DateTimeError;
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
cmp::Ordering,
collections::HashMap,
fmt,
hash::{Hash, Hasher},
ops::{Add, Sub},
str::FromStr,
sync::LazyLock,
};
use time::{
format_description, Date, Duration, Month, OffsetDateTime,
PrimitiveDateTime, Time, UtcOffset, Weekday,
};
mod builder;
#[cfg(test)]
mod tests;
mod validate;
pub use builder::DateTimeBuilder;
pub(super) const MAX_HOUR: u8 = 23;
pub(super) const MAX_MIN_SEC: u8 = 59;
pub(super) const MAX_DAY: u8 = 31;
pub(super) const MAX_MONTH: u8 = 12;
pub(super) const MAX_MICROSECOND: u32 = 999_999;
pub(super) const MAX_ISO_WEEK: u8 = 53;
pub(super) const MAX_ORDINAL_DAY: u16 = 366;
#[derive(Copy, Clone, Debug)]
pub struct DateTime {
pub(crate) datetime: PrimitiveDateTime,
pub(crate) offset: UtcOffset,
}
#[cfg(feature = "serde")]
impl Serialize for DateTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s =
self.format_rfc3339().map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&s)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for DateTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
Self::parse(s).map_err(serde::de::Error::custom)
}
}
static TIMEZONE_OFFSETS: LazyLock<
HashMap<&'static str, Result<UtcOffset, DateTimeError>>,
> = LazyLock::new(|| {
let mut m = HashMap::new();
let _ = m.insert("UTC", Ok(UtcOffset::UTC));
let _ = m.insert("GMT", Ok(UtcOffset::UTC));
let _ = m.insert(
"EST_USA",
UtcOffset::from_hms(-5, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"EDT",
UtcOffset::from_hms(-4, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"CST_USA",
UtcOffset::from_hms(-6, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"CDT",
UtcOffset::from_hms(-5, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"MST",
UtcOffset::from_hms(-7, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"MDT",
UtcOffset::from_hms(-6, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"PST",
UtcOffset::from_hms(-8, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"PDT",
UtcOffset::from_hms(-7, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"CET",
UtcOffset::from_hms(1, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"CEST",
UtcOffset::from_hms(2, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"EET",
UtcOffset::from_hms(2, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"EEST",
UtcOffset::from_hms(3, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"IST_IRELAND",
UtcOffset::from_hms(1, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"IST_ISRAEL",
UtcOffset::from_hms(2, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"JST",
UtcOffset::from_hms(9, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"IST_INDIA",
UtcOffset::from_hms(5, 30, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"CST_CHINA",
UtcOffset::from_hms(8, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"HKT",
UtcOffset::from_hms(8, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"EST_AUS",
UtcOffset::from_hms(10, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"AEDT",
UtcOffset::from_hms(11, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"AEST",
UtcOffset::from_hms(10, 0, 0).map_err(DateTimeError::from),
);
let _ = m.insert(
"ACWST",
UtcOffset::from_hms(8, 45, 0).map_err(DateTimeError::from),
);
m
});
impl DateTime {
#[must_use]
pub fn new() -> Self {
let now = OffsetDateTime::now_utc();
Self {
datetime: PrimitiveDateTime::new(now.date(), now.time()),
offset: UtcOffset::UTC,
}
}
pub fn new_with_tz(tz: &str) -> Result<Self, DateTimeError> {
let offset = TIMEZONE_OFFSETS
.get(tz)
.ok_or(DateTimeError::InvalidTimezone)?
.as_ref()
.map_err(Clone::clone)?;
let now_utc = OffsetDateTime::now_utc();
let now_local = now_utc.to_offset(*offset);
Ok(Self {
datetime: PrimitiveDateTime::new(
now_local.date(),
now_local.time(),
),
offset: *offset,
})
}
pub fn new_with_custom_offset(
hours: i8,
minutes: i8,
) -> Result<Self, DateTimeError> {
if hours.abs() > 23 || minutes.abs() > 59 {
return Err(DateTimeError::InvalidTimezone);
}
if hours != 0
&& minutes != 0
&& hours.signum() != minutes.signum()
{
return Err(DateTimeError::InvalidTimezone);
}
let offset = UtcOffset::from_hms(hours, minutes, 0)
.map_err(|_| DateTimeError::InvalidTimezone)?;
let now_utc = OffsetDateTime::now_utc();
let now_local = now_utc.to_offset(offset);
Ok(Self {
datetime: PrimitiveDateTime::new(
now_local.date(),
now_local.time(),
),
offset,
})
}
pub fn previous_day(&self) -> Result<Self, DateTimeError> {
self.add_days(-1)
}
pub fn next_day(&self) -> Result<Self, DateTimeError> {
self.add_days(1)
}
pub fn set_time(
&self,
hour: u8,
minute: u8,
second: u8,
) -> Result<Self, DateTimeError> {
let new_time = Time::from_hms(hour, minute, second)
.map_err(|_| DateTimeError::InvalidTime)?;
Ok(Self {
datetime: PrimitiveDateTime::new(
self.datetime.date(),
new_time,
),
offset: self.offset,
})
}
pub fn sub_years(&self, years: i32) -> Result<Self, DateTimeError> {
self.add_years(-years)
}
pub fn format_time_in_timezone(
&self,
tz: &str,
format_str: &str,
) -> Result<String, DateTimeError> {
let dt_tz = self.convert_to_tz(tz)?;
dt_tz.format(format_str)
}
#[must_use]
pub fn is_valid_iso_8601(input: &str) -> bool {
if OffsetDateTime::parse(
input,
&format_description::well_known::Rfc3339,
)
.is_ok()
{
return true;
}
if !input.contains('T') && !input.contains(' ') {
return Date::parse(
input,
&format_description::well_known::Iso8601::DATE,
)
.is_ok();
}
false
}
pub fn from_components(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
offset: UtcOffset,
) -> Result<Self, DateTimeError> {
let month = Month::try_from(month)
.map_err(|_| DateTimeError::InvalidDate)?;
let date = Date::from_calendar_date(year, month, day)
.map_err(|_| DateTimeError::InvalidDate)?;
let time = Time::from_hms(hour, minute, second)
.map_err(|_| DateTimeError::InvalidTime)?;
Ok(Self {
datetime: PrimitiveDateTime::new(date, time),
offset,
})
}
#[must_use]
pub const fn year(&self) -> i32 {
self.datetime.date().year()
}
#[must_use]
pub const fn month(&self) -> Month {
self.datetime.date().month()
}
#[must_use]
pub const fn day(&self) -> u8 {
self.datetime.date().day()
}
#[must_use]
pub const fn hour(&self) -> u8 {
self.datetime.time().hour()
}
#[must_use]
pub const fn minute(&self) -> u8 {
self.datetime.time().minute()
}
#[must_use]
pub const fn second(&self) -> u8 {
self.datetime.time().second()
}
#[must_use]
pub const fn microsecond(&self) -> u32 {
self.datetime.microsecond()
}
#[must_use]
pub const fn iso_week(&self) -> u8 {
self.datetime.iso_week()
}
#[must_use]
pub const fn iso_year(&self) -> i32 {
self.datetime.date().to_iso_week_date().0
}
#[must_use]
pub const fn ordinal(&self) -> u16 {
self.datetime.ordinal()
}
#[must_use]
pub const fn offset(&self) -> UtcOffset {
self.offset
}
#[must_use]
pub const fn weekday(&self) -> Weekday {
self.datetime.date().weekday()
}
pub fn parse(input: &str) -> Result<Self, DateTimeError> {
if let Ok(odt) = OffsetDateTime::parse(
input,
&format_description::well_known::Rfc3339,
) {
return Ok(Self {
datetime: PrimitiveDateTime::new(
odt.date(),
odt.time(),
),
offset: odt.offset(),
});
}
if !input.contains('T') && !input.contains(' ') {
if let Ok(date) = Date::parse(
input,
&format_description::well_known::Iso8601::DATE,
) {
return Ok(Self {
datetime: PrimitiveDateTime::new(
date,
Time::MIDNIGHT,
),
offset: UtcOffset::UTC,
});
}
}
Err(DateTimeError::InvalidFormat)
}
pub fn parse_custom_format(
input: &str,
format: &str,
) -> Result<Self, DateTimeError> {
let format_desc = format_description::parse(format)
.map_err(|_| DateTimeError::InvalidFormat)?;
let datetime = PrimitiveDateTime::parse(input, &format_desc)
.map_err(|_| DateTimeError::InvalidFormat)?;
Ok(Self {
datetime,
offset: UtcOffset::UTC,
})
}
pub fn format(
&self,
format_str: &str,
) -> Result<String, DateTimeError> {
let format_desc = format_description::parse(format_str)
.map_err(|_| DateTimeError::InvalidFormat)?;
self.datetime
.format(&format_desc)
.map_err(|_| DateTimeError::InvalidFormat)
}
pub fn format_rfc3339(&self) -> Result<String, DateTimeError> {
self.datetime
.assume_offset(self.offset)
.format(&format_description::well_known::Rfc3339)
.map_err(|_| DateTimeError::InvalidFormat)
}
pub fn update(&self) -> Result<Self, DateTimeError> {
let now = OffsetDateTime::now_utc().to_offset(self.offset);
Ok(Self {
datetime: PrimitiveDateTime::new(now.date(), now.time()),
offset: self.offset,
})
}
pub fn convert_to_tz(
&self,
new_tz: &str,
) -> Result<Self, DateTimeError> {
let new_offset = TIMEZONE_OFFSETS
.get(new_tz)
.ok_or(DateTimeError::InvalidTimezone)?
.as_ref()
.map_err(Clone::clone)?;
let datetime_with_offset =
self.datetime.assume_offset(self.offset);
let new_datetime = datetime_with_offset.to_offset(*new_offset);
Ok(Self {
datetime: PrimitiveDateTime::new(
new_datetime.date(),
new_datetime.time(),
),
offset: *new_offset,
})
}
#[must_use]
pub const fn unix_timestamp(&self) -> i64 {
self.datetime.assume_offset(self.offset).unix_timestamp()
}
#[must_use]
pub fn duration_since(&self, other: &Self) -> Duration {
let self_offset = self.datetime.assume_offset(self.offset);
let other_offset = other.datetime.assume_offset(other.offset);
let seconds_diff = self_offset.unix_timestamp()
- other_offset.unix_timestamp();
let nanos_diff = i64::from(self_offset.nanosecond())
- i64::from(other_offset.nanosecond());
Duration::seconds(seconds_diff)
+ Duration::nanoseconds(nanos_diff)
}
pub fn add_days(&self, days: i64) -> Result<Self, DateTimeError> {
let new_datetime = self
.datetime
.checked_add(Duration::days(days))
.ok_or(DateTimeError::InvalidDate)?;
Ok(Self {
datetime: new_datetime,
offset: self.offset,
})
}
pub fn add_months(
&self,
months: i32,
) -> Result<Self, DateTimeError> {
let current_date = self.datetime.date();
let total_months = current_date
.year()
.checked_mul(12)
.and_then(|v| {
v.checked_add(i32::from(current_date.month() as u8))
})
.and_then(|v| v.checked_sub(1))
.and_then(|v| v.checked_add(months))
.ok_or(DateTimeError::InvalidDate)?;
let target_year = total_months.div_euclid(12);
let target_month =
u8::try_from(total_months.rem_euclid(12) + 1);
let target_month =
target_month.map_err(|_| DateTimeError::InvalidDate)?;
let days_in_target_month =
days_in_month(target_year, target_month)?;
let target_day = current_date.day().min(days_in_target_month);
let new_month = Month::try_from(target_month)
.map_err(|_| DateTimeError::InvalidDate)?;
let new_date = Date::from_calendar_date(
target_year,
new_month,
target_day,
)
.map_err(|_| DateTimeError::InvalidDate)?;
Ok(Self {
datetime: PrimitiveDateTime::new(
new_date,
self.datetime.time(),
),
offset: self.offset,
})
}
pub fn sub_months(
&self,
months: i32,
) -> Result<Self, DateTimeError> {
self.add_months(-months)
}
pub fn add_years(&self, years: i32) -> Result<Self, DateTimeError> {
let current_date = self.datetime.date();
let target_year = current_date
.year()
.checked_add(years)
.ok_or(DateTimeError::InvalidDate)?;
let new_day = if current_date.month() == Month::February
&& current_date.day() == 29
&& !is_leap_year(target_year)
{
28
} else {
current_date.day()
};
let new_date = Date::from_calendar_date(
target_year,
current_date.month(),
new_day,
)
.map_err(|_| DateTimeError::InvalidDate)?;
Ok(Self {
datetime: PrimitiveDateTime::new(
new_date,
self.datetime.time(),
),
offset: self.offset,
})
}
pub fn start_of_week(&self) -> Result<Self, DateTimeError> {
let days_since_monday = i64::from(
self.datetime.weekday().number_days_from_monday(),
);
self.add_days(-days_since_monday)
}
pub fn end_of_week(&self) -> Result<Self, DateTimeError> {
let days_until_sunday = 6 - i64::from(
self.datetime.weekday().number_days_from_monday(),
);
self.add_days(days_until_sunday)
}
pub fn start_of_month(&self) -> Result<Self, DateTimeError> {
self.set_date(
self.datetime.year(),
self.datetime.month() as u8,
1,
)
}
pub fn end_of_month(&self) -> Result<Self, DateTimeError> {
let year = self.datetime.year();
let month = self.datetime.month() as u8;
let last_day = days_in_month(year, month)?;
self.set_date(year, month, last_day)
}
pub fn start_of_year(&self) -> Result<Self, DateTimeError> {
self.set_date(self.datetime.year(), 1, 1)
}
pub fn end_of_year(&self) -> Result<Self, DateTimeError> {
self.set_date(self.datetime.year(), 12, 31)
}
#[must_use]
pub fn is_within_range(&self, start: &Self, end: &Self) -> bool {
self >= start && self <= end
}
pub fn set_date(
&self,
year: i32,
month: u8,
day: u8,
) -> Result<Self, DateTimeError> {
let month = Month::try_from(month)
.map_err(|_| DateTimeError::InvalidDate)?;
let new_date = Date::from_calendar_date(year, month, day)
.map_err(|_| DateTimeError::InvalidDate)?;
Ok(Self {
datetime: PrimitiveDateTime::new(
new_date,
self.datetime.time(),
),
offset: self.offset,
})
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.format_rfc3339()
.map_or(Err(fmt::Error), |s| write!(f, "{s}"))
}
}
impl FromStr for DateTime {
type Err = DateTimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl Default for DateTime {
fn default() -> Self {
let date = Date::from_calendar_date(1970, Month::January, 1)
.unwrap_or(Date::MIN);
Self {
datetime: PrimitiveDateTime::new(date, Time::MIDNIGHT),
offset: UtcOffset::UTC,
}
}
}
impl Add<Duration> for DateTime {
type Output = Result<Self, DateTimeError>;
fn add(self, rhs: Duration) -> Self::Output {
let maybe_new = self.datetime.checked_add(rhs);
maybe_new.map_or(
Err(DateTimeError::InvalidDate),
|new_datetime| {
Ok(Self {
datetime: new_datetime,
offset: self.offset,
})
},
)
}
}
impl Sub<Duration> for DateTime {
type Output = Result<Self, DateTimeError>;
fn sub(self, rhs: Duration) -> Self::Output {
let maybe_new = self.datetime.checked_sub(rhs);
maybe_new.map_or(
Err(DateTimeError::InvalidDate),
|new_datetime| {
Ok(Self {
datetime: new_datetime,
offset: self.offset,
})
},
)
}
}
impl PartialEq for DateTime {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for DateTime {}
impl PartialOrd for DateTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DateTime {
fn cmp(&self, other: &Self) -> Ordering {
let self_utc = self.datetime.assume_offset(self.offset);
let other_utc = other.datetime.assume_offset(other.offset);
self_utc.cmp(&other_utc)
}
}
impl Hash for DateTime {
fn hash<H: Hasher>(&self, state: &mut H) {
self.datetime
.assume_offset(self.offset)
.unix_timestamp()
.hash(state);
self.datetime
.assume_offset(self.offset)
.nanosecond()
.hash(state);
}
}
pub const fn days_in_month(
year: i32,
month: u8,
) -> Result<u8, DateTimeError> {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => Ok(31),
4 | 6 | 9 | 11 => Ok(30),
2 => Ok(if is_leap_year(year) { 29 } else { 28 }),
_ => Err(DateTimeError::InvalidDate),
}
}
#[must_use]
pub const fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}