#![deny(
missing_docs,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::result_unit_err,
clippy::clone_on_ref_ptr
)]
#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
use crate::error::DateTimeError;
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering,
collections::HashMap,
fmt,
hash::{Hash, Hasher},
ops::{Add, Sub},
str::FromStr,
};
use time::{
format_description, Date, Duration, Month, OffsetDateTime,
PrimitiveDateTime, Time, UtcOffset, Weekday,
};
const MAX_HOUR: u8 = 23;
const MAX_MIN_SEC: u8 = 59;
const MAX_DAY: u8 = 31;
const MAX_MONTH: u8 = 12;
const MAX_MICROSECOND: u32 = 999_999;
const MAX_ISO_WEEK: u8 = 53;
const MAX_ORDINAL_DAY: u16 = 366;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct DateTime {
pub datetime: PrimitiveDateTime,
pub offset: UtcOffset,
}
lazy_static::lazy_static! {
static ref TIMEZONE_OFFSETS: HashMap<&'static str, Result<UtcOffset, DateTimeError>> = {
let mut m = HashMap::new();
let _ = m.insert("UTC", Ok(UtcOffset::UTC));
let _ = m.insert("GMT", Ok(UtcOffset::UTC));
let _ = m.insert("EST", 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", 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("JST", UtcOffset::from_hms(9, 0, 0).map_err(DateTimeError::from));
let _ = m.insert("IST", UtcOffset::from_hms(5, 30, 0).map_err(DateTimeError::from));
let _ = m.insert("HKT", UtcOffset::from_hms(8, 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(
"WADT",
UtcOffset::from_hms(8, 45, 0)
.map_err(DateTimeError::from)
);
m
};
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
pub struct DateTimeBuilder {
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
offset: UtcOffset,
}
impl Default for DateTimeBuilder {
fn default() -> Self {
Self {
year: 1970,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
offset: UtcOffset::UTC,
}
}
}
impl DateTimeBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
year: 1970,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
offset: UtcOffset::UTC,
}
}
#[must_use]
pub const fn year(mut self, year: i32) -> Self {
self.year = year;
self
}
#[must_use]
pub const fn month(mut self, month: u8) -> Self {
self.month = month;
self
}
#[must_use]
pub const fn day(mut self, day: u8) -> Self {
self.day = day;
self
}
#[must_use]
pub const fn hour(mut self, hour: u8) -> Self {
self.hour = hour;
self
}
#[must_use]
pub const fn minute(mut self, minute: u8) -> Self {
self.minute = minute;
self
}
#[must_use]
pub const fn second(mut self, second: u8) -> Self {
self.second = second;
self
}
#[must_use]
pub const fn offset(mut self, offset: UtcOffset) -> Self {
self.offset = offset;
self
}
pub fn build(&self) -> Result<DateTime, DateTimeError> {
DateTime::from_components(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
self.offset,
)
}
}
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);
}
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 PrimitiveDateTime::parse(
input,
&format_description::well_known::Rfc3339,
)
.is_ok()
{
return true;
}
if Date::parse(
input,
&format_description::well_known::Iso8601::DATE,
)
.is_ok()
{
return true;
}
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 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(dt) = PrimitiveDateTime::parse(
input,
&format_description::well_known::Rfc3339,
) {
return Ok(Self {
datetime: dt,
offset: UtcOffset::UTC,
});
}
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 format_iso8601(&self) -> Result<String, DateTimeError> {
self.format("[year]-[month]-[day]T[hour]:[minute]:[second]")
}
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() * 12 + current_date.month() as i32 - 1
+ months;
let target_year = total_months / 12;
let target_month = u8::try_from((total_months % 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 DateTime {
#[must_use]
pub fn is_valid_day(day: &str) -> bool {
day.parse::<u8>()
.map(|d| (1..=MAX_DAY).contains(&d))
.unwrap_or(false)
}
#[must_use]
pub fn is_valid_hour(hour: &str) -> bool {
hour.parse::<u8>().map(|h| h <= MAX_HOUR).unwrap_or(false)
}
#[must_use]
pub fn is_valid_minute(minute: &str) -> bool {
minute
.parse::<u8>()
.map(|m| m <= MAX_MIN_SEC)
.unwrap_or(false)
}
#[must_use]
pub fn is_valid_second(second: &str) -> bool {
second
.parse::<u8>()
.map(|s| s <= MAX_MIN_SEC)
.unwrap_or(false)
}
#[must_use]
pub fn is_valid_month(month: &str) -> bool {
month
.parse::<u8>()
.map(|m| (1..=MAX_MONTH).contains(&m))
.unwrap_or(false)
}
#[must_use]
pub fn is_valid_year(year: &str) -> bool {
year.parse::<i32>().is_ok()
}
#[must_use]
pub fn is_valid_microsecond(microsecond: &str) -> bool {
microsecond
.parse::<u32>()
.map(|us| us <= MAX_MICROSECOND)
.unwrap_or(false)
}
#[must_use]
pub fn is_valid_ordinal(ordinal: &str) -> bool {
ordinal
.parse::<u16>()
.map(|o| (1..=MAX_ORDINAL_DAY).contains(&o))
.unwrap_or(false)
}
#[must_use]
pub fn is_valid_iso_week(week: &str) -> bool {
week.parse::<u8>()
.map(|w| (1..=MAX_ISO_WEEK).contains(&w))
.unwrap_or(false)
}
#[must_use]
pub fn is_valid_time(time: &str) -> bool {
let parts: Vec<&str> = time.split(':').collect();
if parts.len() != 3 {
return false;
}
Self::is_valid_hour(parts[0])
&& Self::is_valid_minute(parts[1])
&& Self::is_valid_second(parts[2])
}
}
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 {
Self::new()
}
}
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 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 {
self.datetime.cmp(&other.datetime)
}
}
impl Hash for DateTime {
fn hash<H: Hasher>(&self, state: &mut H) {
self.datetime.hash(state);
self.offset.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)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_new() {
let dt = DateTime::new();
assert_eq!(dt.offset(), UtcOffset::UTC);
}
#[test]
fn test_new_with_tz() {
let est = DateTime::new_with_tz("EST");
assert!(est.is_ok());
if let Ok(est_dt) = est {
assert_eq!(est_dt.offset().whole_hours(), -5);
}
let invalid = DateTime::new_with_tz("INVALID");
assert!(matches!(invalid, Err(DateTimeError::InvalidTimezone)));
}
#[test]
fn test_new_with_custom_offset() {
let offset = DateTime::new_with_custom_offset(5, 30);
assert!(offset.is_ok());
if let Ok(dt) = offset {
assert_eq!(dt.offset().whole_hours(), 5);
assert_eq!(dt.offset().minutes_past_hour(), 30);
}
let too_large_hours = DateTime::new_with_custom_offset(24, 0);
assert!(too_large_hours.is_err());
let too_large_minutes = DateTime::new_with_custom_offset(0, 60);
assert!(too_large_minutes.is_err());
}
#[test]
fn test_from_components() {
let dt = DateTime::from_components(
2024,
1,
1,
12,
0,
0,
UtcOffset::UTC,
);
assert!(dt.is_ok());
if let Ok(dt_val) = dt {
assert_eq!(dt_val.year(), 2024);
assert_eq!(dt_val.month(), Month::January);
assert_eq!(dt_val.day(), 1);
assert_eq!(dt_val.hour(), 12);
assert_eq!(dt_val.minute(), 0);
assert_eq!(dt_val.second(), 0);
}
let invalid_month = DateTime::from_components(
2024,
13,
1,
0,
0,
0,
UtcOffset::UTC,
);
assert!(invalid_month.is_err());
let invalid_day = DateTime::from_components(
2024,
2,
30,
0,
0,
0,
UtcOffset::UTC,
);
assert!(invalid_day.is_err());
}
#[test]
fn test_parse() {
let dt = DateTime::parse("2024-01-01T12:00:00Z");
assert!(dt.is_ok());
let dt = DateTime::parse("2024-01-01");
assert!(dt.is_ok());
if let Ok(dt_val) = dt {
assert_eq!(dt_val.hour(), 0);
assert_eq!(dt_val.minute(), 0);
}
let invalid1 = DateTime::parse("invalid");
assert!(invalid1.is_err());
let invalid2 = DateTime::parse("2024-13-01");
assert!(invalid2.is_err());
}
#[test]
fn test_format() {
let dt = DateTime::new();
let maybe_formatted = dt.format("[year]-[month]-[day]");
assert!(maybe_formatted.is_ok());
let invalid_format = dt.format("[invalid]");
assert!(invalid_format.is_err());
}
#[test]
fn test_timezone_conversion() {
let utc = DateTime::new();
let est = utc.convert_to_tz("EST");
assert!(est.is_ok());
if let Ok(est_val) = est {
assert_eq!(est_val.offset().whole_hours(), -5);
}
let invalid = utc.convert_to_tz("INVALID");
assert!(invalid.is_err());
}
#[test]
fn test_arithmetic() {
let dt = DateTime::new();
let future = dt.add_days(7);
assert!(future.is_ok());
let past = dt.add_days(-7);
assert!(past.is_ok());
let next_month = dt.add_months(1);
assert!(next_month.is_ok());
let jan31 = DateTime::from_components(
2024,
1,
31,
0,
0,
0,
UtcOffset::UTC,
);
assert!(jan31.is_ok());
if let Ok(jan31_dt) = jan31 {
let feb = jan31_dt.add_months(1);
assert!(feb.is_ok());
if let Ok(feb_dt) = feb {
assert_eq!(feb_dt.day(), 29);
}
}
}
#[test]
fn test_leap_year() {
assert!(is_leap_year(2024));
assert!(!is_leap_year(2023));
assert!(is_leap_year(2000));
assert!(!is_leap_year(1900));
}
#[test]
fn test_validation() {
assert!(DateTime::is_valid_day("1"));
assert!(DateTime::is_valid_day("31"));
assert!(!DateTime::is_valid_day("0"));
assert!(!DateTime::is_valid_day("32"));
assert!(!DateTime::is_valid_day("abc"));
assert!(DateTime::is_valid_hour("0"));
assert!(DateTime::is_valid_hour("23"));
assert!(!DateTime::is_valid_hour("24"));
assert!(DateTime::is_valid_minute("0"));
assert!(DateTime::is_valid_minute("59"));
assert!(!DateTime::is_valid_minute("60"));
assert!(DateTime::is_valid_time("00:00:00"));
assert!(DateTime::is_valid_time("23:59:59"));
assert!(!DateTime::is_valid_time("24:00:00"));
assert!(!DateTime::is_valid_time("23:60:00"));
assert!(!DateTime::is_valid_time("23:59:60"));
}
#[test]
fn test_range_operations() {
let dt = DateTime::from_components(
2024,
1,
15,
12,
0,
0,
UtcOffset::UTC,
);
assert!(dt.is_ok());
if let Ok(dt_val) = dt {
let week_start = dt_val.start_of_week();
assert!(week_start.is_ok());
if let Ok(ws) = week_start {
assert_eq!(ws.weekday(), Weekday::Monday);
}
let week_end = dt_val.end_of_week();
assert!(week_end.is_ok());
if let Ok(we) = week_end {
assert_eq!(we.weekday(), Weekday::Sunday);
}
let month_start = dt_val.start_of_month();
assert!(month_start.is_ok());
if let Ok(ms) = month_start {
assert_eq!(ms.day(), 1);
}
let month_end = dt_val.end_of_month();
assert!(month_end.is_ok());
if let Ok(me) = month_end {
assert_eq!(me.day(), 31);
}
let year_start = dt_val.start_of_year();
assert!(year_start.is_ok());
if let Ok(ys) = year_start {
assert_eq!(ys.month(), Month::January);
assert_eq!(ys.day(), 1);
}
let year_end = dt_val.end_of_year();
assert!(year_end.is_ok());
if let Ok(ye) = year_end {
assert_eq!(ye.month(), Month::December);
assert_eq!(ye.day(), 31);
}
}
}
#[test]
fn test_ordering() {
let dt1 = DateTime::from_components(
2024,
1,
1,
0,
0,
0,
UtcOffset::UTC,
);
let dt2 = DateTime::from_components(
2024,
1,
2,
0,
0,
0,
UtcOffset::UTC,
);
assert!(dt1.is_ok());
assert!(dt2.is_ok());
if let (Ok(a), Ok(b)) = (dt1, dt2) {
assert!(a < b);
assert!(b > a);
assert_ne!(a, b);
}
}
#[test]
fn test_duration() {
let dt1 = DateTime::from_components(
2024,
1,
1,
0,
0,
0,
UtcOffset::UTC,
);
let dt2 = DateTime::from_components(
2024,
1,
2,
0,
0,
0,
UtcOffset::UTC,
);
if let (Ok(a), Ok(b)) = (dt1, dt2) {
let duration = b.duration_since(&a);
assert_eq!(duration.whole_days(), 1);
}
}
#[test]
fn test_from_str() {
let dt = DateTime::from_str("2024-01-01T00:00:00Z");
assert!(dt.is_ok());
let invalid = DateTime::from_str("invalid");
assert!(invalid.is_err());
}
#[test]
fn test_display() {
let dt = DateTime::from_components(
2024,
1,
1,
0,
0,
0,
UtcOffset::UTC,
);
assert!(dt.is_ok());
if let Ok(dt_val) = dt {
assert_eq!(dt_val.to_string(), "2024-01-01T00:00:00Z");
}
}
#[test]
fn test_hash() {
use std::collections::HashSet;
let dt1 = DateTime::from_components(
2024,
1,
1,
0,
0,
0,
UtcOffset::UTC,
);
let dt2 = DateTime::from_components(
2024,
1,
1,
0,
0,
0,
UtcOffset::UTC,
);
assert!(dt1.is_ok());
assert!(dt2.is_ok());
if let (Ok(a), Ok(b)) = (dt1, dt2) {
let mut set = HashSet::new();
assert!(
set.insert(a),
"The set should not have contained `a` before"
);
assert!(set.contains(&b));
}
}
#[test]
fn test_builder_pattern() {
let builder = DateTimeBuilder::new()
.year(2024)
.month(1)
.day(1)
.hour(12)
.minute(30)
.second(45)
.offset(UtcOffset::UTC);
let dt = builder.build();
assert!(dt.is_ok());
if let Ok(value) = dt {
assert_eq!(value.year(), 2024);
assert_eq!(value.month(), Month::January);
assert_eq!(value.day(), 1);
assert_eq!(value.hour(), 12);
assert_eq!(value.minute(), 30);
assert_eq!(value.second(), 45);
}
}
}