use {
core::{
cmp::Ordering,
fmt::{self, Display, Formatter},
time::Duration,
},
crate::{Error, Result as CrateResult},
};
#[cfg(feature="unix")]
use {
core::{
convert::TryFrom,
ptr,
},
std::time::{SystemTime, UNIX_EPOCH},
};
const END_OF_FEBRUARY_IN_LEAP_YEARS: u8 = 29;
const END_OF_FEBRUARY_IN_COMMON_YEARS: u8 = 28;
const MAX_GMT_OFFSET: i32 = 14 * 3600;
const MIN_GMT_OFFSET: i32 = -12 * 3600;
#[cfg(feature="unix")]
const UNIX_EPOCH_YEAR: i32 = 1900;
#[cfg(feature="unix")]
#[derive(Debug)]
enum TimeKind {
UTC,
Local,
}
#[derive(Debug, Eq, Hash, PartialOrd, Clone)]
pub struct Time {
year: i64,
month: Month,
day: u8,
hour: u8,
minute: u8,
second: u8,
gmt_offset: Option<i32>,
}
impl Time {
pub fn make(year: i64, month: Month, day: u8, hour: u8, minute: u8, second: u8, gmt_offset: Option<i32>) -> CrateResult<Self> {
match month {
Month::January | Month::March | Month::May | Month::July | Month::August | Month::October | Month::December
=> if day == 0 || day > 31 {
return Err(Error::from(__!("Invalid day: {day} of {month}", day=day, month=month)));
},
Month::April | Month::June | Month::September | Month::November => if day == 0 || day > 30 {
return Err(Error::from(__!("Invalid day: {day} of {month}", day=day, month=month)));
},
Month::February
=> if day == 0 || day > if is_leap_year(year) { END_OF_FEBRUARY_IN_LEAP_YEARS } else { END_OF_FEBRUARY_IN_COMMON_YEARS } {
return Err(Error::from(__!("Invalid day: {day} of {month}", day=day, month=month)));
},
};
if hour > 23 {
return Err(Error::from(__!("Invalid hour: {hour}", hour=hour)));
}
if minute > 59 {
return Err(Error::from(__!("Invalid minute: {minute}", minute=minute)));
}
if second > 59 {
return Err(Error::from(__!("Invalid second: {second}", second=second)));
}
if let Some(gmt_offset) = gmt_offset.as_ref() {
if gmt_offset > &MAX_GMT_OFFSET || gmt_offset < &MIN_GMT_OFFSET {
return Err(Error::from(__!("Invalid GMT offset: {gmt_offset}", gmt_offset=gmt_offset)));
}
}
Ok(Self {
year, month, day,
hour, minute, second,
gmt_offset,
})
}
pub const fn year(&self) -> i64 {
self.year
}
pub const fn month(&self) -> Month {
self.month
}
pub const fn day(&self) -> u8 {
self.day
}
pub const fn hour(&self) -> u8 {
self.hour
}
pub const fn minute(&self) -> u8 {
self.minute
}
pub const fn second(&self) -> u8 {
self.second
}
pub const fn gmt_offset(&self) -> Option<i32> {
self.gmt_offset
}
#[cfg(feature="unix")]
pub fn make_utc() -> CrateResult<Self> {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(duration) => Self::try_from(duration),
Err(err) => {
let unix_seconds = err.duration().as_secs();
let unix_seconds = i64::try_from(unix_seconds)
.map_err(|_| Error::from(__!("Failed to convert '{}' into Unix seconds", unix_seconds)))?;
try_unix_seconds_into_time(-unix_seconds, TimeKind::UTC)
},
}
}
pub fn try_into_utc(&self) -> CrateResult<Self> {
match self.gmt_offset {
None => Ok(self.clone()),
Some(0) => Ok(Self {
gmt_offset: None,
..*self
}),
Some(gmt_offset) => {
let today_seconds = (crate::HOUR * u64::from(self.hour) + crate::MINUTE * u64::from(self.minute) + u64::from(self.second))
as i32;
let seconds = today_seconds - gmt_offset;
let (year, month, day, hour, minute, second) = if seconds < 0 {
let (year, month, day) = match self.day {
1 => {
let year = match self.month {
Month::January => self.year.checked_sub(1)
.ok_or_else(|| Error::from(__!("Failed to subtract '{year}' by 1", year=self.year)))?,
_ => self.year,
};
let month = self.month.wrapping_last();
let day = last_day_of_month(&month, is_leap_year(year));
(year, month, day)
},
_ => (self.year, self.month, self.day - 1),
};
let (_, hour, minute, second) = crate::duration_to_dhms(&Duration::from_secs(crate::DAY - seconds.abs() as u64));
(year, month, day, hour, minute, second)
} else if seconds < crate::DAY as i32 {
let (_, hour, minute, second) = crate::duration_to_dhms(&Duration::from_secs(seconds as u64));
(self.year, self.month, self.day, hour, minute, second)
} else {
let (year, month, day) = if self.day == last_day_of_month(&self.month, is_leap_year(self.year)) {
let year = match self.month {
Month::December => self.year.checked_add(1)
.ok_or_else(|| Error::from(__!("Failed to add 1 to '{year}'", year=self.year)))?,
_ => self.year,
};
(year, self.month.wrapping_next(), 1)
} else {
(self.year, self.month, self.day + 1)
};
let (_, hour, minute, second) = crate::duration_to_dhms(&Duration::from_secs(seconds as u64 - crate::DAY));
(year, month, day, hour, minute, second)
};
Self::make(year, month, day, hour as u8, minute as u8, second as u8, None)
},
}
}
pub fn is_utc(&self) -> bool {
matches!(&self.gmt_offset, Some(0) | None)
}
#[cfg(feature="unix")]
pub fn make_local() -> CrateResult<Self> {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(duration) => try_duration_into_local_time(&duration),
Err(err) => {
let unix_seconds = err.duration().as_secs();
let unix_seconds = i64::try_from(unix_seconds)
.map_err(|_| Error::from(__!("Failed to convert '{}' into Unix seconds", unix_seconds)))?;
try_unix_seconds_into_time(-unix_seconds, TimeKind::Local)
},
}
}
#[cfg(feature="unix")]
pub fn try_into_local(&self) -> CrateResult<Self> {
try_duration_into_local_time(&Duration::try_from(self)?)
}
}
impl PartialEq for Time {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Ord for Time {
fn cmp(&self, other: &Self) -> Ordering {
let self_seconds = match self.year.checked_sub(other.year) {
Some(0) => match self.month.to_unix() - other.month.to_unix() {
0 => match self.day as i8 - other.day as i8 {
0 => 0_i32,
-1 => -(crate::DAY as i32),
1 => crate::DAY as i32,
other => return if other > 0 { Ordering::Greater } else { Ordering::Less },
},
month_delta => {
let is_leap_year = is_leap_year(self.year);
match month_delta {
-1 => if last_day_of_month(&self.month, is_leap_year) == self.day && other.day == 1 {
return Ordering::Less;
} else {
-(crate::DAY as i32)
},
1 => if self.day == 1 && last_day_of_month(&other.month, is_leap_year) == other.day {
return Ordering::Greater;
} else {
crate::DAY as i32
},
other => return if other > 0 { Ordering::Greater } else { Ordering::Less },
}
},
},
Some(1) => match (self.month, self.day, other.month, other.day) {
(Month::January, 1, Month::December, 31) => crate::DAY as i32,
_ => return Ordering::Greater,
},
Some(-1) => match (self.month, self.day, other.month, other.day) {
(Month::December, 31, Month::January, 1) => -(crate::DAY as i32),
_ => return Ordering::Less,
},
_ => return self.year.cmp(&other.year),
};
let self_seconds = self_seconds
+ (crate::HOUR * u64::from(self.hour) + crate::MINUTE * u64::from(self.minute) + u64::from(self.second)) as i32
- self.gmt_offset.unwrap_or(0);
let other_seconds =
(crate::HOUR * u64::from(other.hour) + crate::MINUTE * u64::from(other.minute) + u64::from(other.second)) as i32
- other.gmt_offset.unwrap_or(0);
self_seconds.cmp(&other_seconds)
}
}
#[cfg(feature="unix")]
impl TryFrom<&Time> for Duration {
type Error = Error;
fn try_from(time: &Time) -> Result<Self, Self::Error> {
let time = time.try_into_utc()?;
let mut tm = libc::tm {
tm_year: match time.year.checked_sub(UNIX_EPOCH_YEAR.into()).map(|y| i32::try_from(y)) {
Some(Ok(tm_year)) => tm_year,
_ => return Err(Error::from(__!("'{year}' cannot be converted into Unix year", year=time.year))),
},
tm_mon: time.month.to_unix(),
tm_mday: time.day.into(),
tm_hour: time.hour.into(),
tm_min: time.minute.into(),
tm_sec: time.second.into(),
tm_wday: 0, tm_yday: 0, tm_isdst: -1, tm_gmtoff: 0, tm_zone: ptr::null(),
};
match unsafe {
libc::mktime(&mut tm)
} {
-1 => Err(Error::from(__!("libc::mktime() returned -1"))),
unix_seconds => match unix_seconds.checked_add(tm.tm_gmtoff) {
Some(unix_seconds) => u64::try_from(unix_seconds).map(|s| Duration::from_secs(s)).map_err(|_| Error::from(__!(
"Failed to convert '{unix_seconds}' into u64",
unix_seconds=unix_seconds,
))),
None => Err(Error::from(__!(
"Failed to calculate: '{unix_seconds} + {tm_gmtoff}'",
unix_seconds=unix_seconds, tm_gmtoff=tm.tm_gmtoff,
)))?,
},
}
}
}
#[cfg(feature="unix")]
impl TryFrom<Time> for Duration {
type Error = Error;
fn try_from(time: Time) -> Result<Self, Self::Error> {
Self::try_from(&time)
}
}
#[cfg(feature="unix")]
impl TryFrom<&Duration> for Time {
type Error = Error;
fn try_from(duration: &Duration) -> Result<Self, Self::Error> {
try_duration_into_time(duration, TimeKind::UTC)
}
}
#[cfg(feature="unix")]
impl TryFrom<Duration> for Time {
type Error = Error;
fn try_from(duration: Duration) -> Result<Self, Self::Error> {
Self::try_from(&duration)
}
}
#[cfg(feature="unix")]
fn try_unix_seconds_into_time(unix_seconds: i64, time_kind: TimeKind) -> CrateResult<Time> {
let mut tm = libc::tm {
tm_year: 0, tm_mon: 0, tm_mday: 0,
tm_hour: 0, tm_min: 0, tm_sec: 0,
tm_wday: 0, tm_yday: 0, tm_isdst: 0, tm_gmtoff: 0, tm_zone: ptr::null(),
};
let (ptr, fn_name) = unsafe {
match time_kind {
TimeKind::UTC => (libc::gmtime_r(&unix_seconds, &mut tm), "gmtime_r"),
TimeKind::Local => (libc::localtime_r(&unix_seconds, &mut tm), "localtime_r"),
}
};
if ptr.is_null() {
return Err(Error::from(__!("libc::{fn_name}() returned null", fn_name=fn_name)));
}
let year = i64::from(tm.tm_year) + i64::from(UNIX_EPOCH_YEAR);
let day = u8::try_from(tm.tm_mday).map_err(|_| Error::from(__!("Invalid Unix day: '{day}'", day=tm.tm_mday)))?;
let hour = u8::try_from(tm.tm_hour).map_err(|_| Error::from(__!("Invalid Unix hour: '{hour}'", hour=tm.tm_hour)))?;
let minute = u8::try_from(tm.tm_min).map_err(|_| Error::from(__!("Invalid Unix minute: '{minute}'", minute=tm.tm_min)))?;
let second = u8::try_from(tm.tm_sec).map_err(|_| Error::from(__!("Invalid Unix second: '{second}'", second=tm.tm_sec)))?;
let gmt_offset = match time_kind {
TimeKind::UTC => None,
TimeKind::Local => Some(i32::try_from(tm.tm_gmtoff).map_err(|_| Error::from(__!("Invalid Unix GMT offset: {}", tm.tm_gmtoff)))?),
};
Time::make(year, Month::try_from_unix(tm.tm_mon)?, day, hour, minute, second, gmt_offset)
}
#[cfg(feature="unix")]
fn try_duration_into_time(duration: &Duration, time_kind: TimeKind) -> CrateResult<Time> {
let unix_seconds = duration.as_secs();
let unix_seconds = i64::try_from(unix_seconds)
.map_err(|_| Error::from(__!("Failed to convert {} into i64", unix_seconds)))?;
try_unix_seconds_into_time(unix_seconds, time_kind)
}
#[cfg(feature="unix")]
pub fn try_duration_into_local_time(duration: &Duration) -> CrateResult<Time> {
try_duration_into_time(duration, TimeKind::Local)
}
#[test]
#[cfg(feature="unix")]
fn test_times() -> CrateResult<()> {
let utc = Time::make_utc()?;
std::println!(
"UTC : {month} {day}, {year:04}, {hour:02}:{minute:02}:{second:02} ({gmt_offset:?})",
month=utc.month, day=utc.day, year=utc.year, hour=utc.hour, minute=utc.minute, second=utc.second, gmt_offset=utc.gmt_offset,
);
let local = Time::make_local()?;
std::println!(
"Local : {month} {day}, {year:04}, {hour:02}:{minute:02}:{second:02} ({gmt_offset:?})",
month=local.month, day=local.day, year=local.year, hour=local.hour, minute=local.minute, second=local.second,
gmt_offset=local.gmt_offset,
);
Ok(())
}
#[test]
#[cfg(feature="unix")]
fn test_trying_local_times_from_durations() -> CrateResult<()> {
let gmt_offset = {
let mut tm = libc::tm {
tm_year: 0, tm_mon: 0, tm_mday: 0,
tm_hour: 0, tm_min: 0, tm_sec: 0,
tm_wday: 0, tm_yday: 0, tm_isdst: 0, tm_gmtoff: 0, tm_zone: ptr::null(),
};
assert_eq!(false, unsafe {
libc::localtime_r(&0, &mut tm).is_null()
});
i32::try_from(tm.tm_gmtoff).unwrap()
};
let gmt_offset_is_positive = gmt_offset >= 0;
let (_, hour, minute, second) = if gmt_offset_is_positive {
crate::duration_to_dhms(&Duration::from_secs(u64::try_from(gmt_offset).unwrap()))
} else {
crate::duration_to_dhms(&Duration::from_secs(crate::DAY.checked_sub(u64::try_from(gmt_offset.abs()).unwrap()).unwrap()))
};
let hour = u8::try_from(hour).unwrap();
let minute = u8::try_from(minute).unwrap();
let second = u8::try_from(second).unwrap();
let time = try_duration_into_local_time(&Duration::from_secs(0))?;
let (year, month, day) = if gmt_offset_is_positive {
(1970, Month::January, 1)
} else {
(1969, Month::December, 31)
};
assert_eq!(time.year, year);
assert_eq!(time.month, month);
assert_eq!(time.day, day);
assert_eq!(time.hour, hour);
assert_eq!(time.minute, minute);
assert_eq!(time.second, second);
assert_eq!(time.gmt_offset, Some(gmt_offset));
let time = try_duration_into_local_time(&Duration::from_secs(crate::WEEK))?;
let day = if gmt_offset_is_positive { 8 } else { 7 };
assert_eq!(time.year, 1970);
assert_eq!(time.month, Month::January);
assert_eq!(time.day, day);
assert_eq!(time.hour, hour);
assert_eq!(time.minute, minute);
assert_eq!(time.second, second);
assert_eq!(time.gmt_offset, Some(gmt_offset));
Ok(())
}
fn is_leap_year(year: i64) -> bool {
match year % 4 {
0 => match year % 100 {
0 => year % 400 == 0,
_ => true,
}
_ => false,
}
}
#[test]
fn test_leap_years() {
for y in &[1_i64, 2, 3, 5, 1700, 1800, 1900] {
assert!(is_leap_year(*y) == false);
}
for y in &[4_i64, 8, 1600, 2000] {
assert!(is_leap_year(*y));
}
assert_eq!(END_OF_FEBRUARY_IN_LEAP_YEARS, 29);
assert_eq!(END_OF_FEBRUARY_IN_COMMON_YEARS, 28);
}
#[test]
#[cfg(feature="unix")]
fn test_trying_durations_from_times() -> CrateResult<()> {
for (day, hour, minute, second, gmt_offset) in vec![(1_u8, 0_u8, 1_u8, 59_u8, 0_i32), (9, 1, 2, 59, 3600), (31, 23, 30, 1, -3600)] {
let duration = Duration::try_from(Time::make(1970, Month::January, day, hour, minute, second, Some(gmt_offset))?)?;
let secs = u64::from(day.checked_sub(1).unwrap()).checked_mul(crate::DAY).unwrap()
+ u64::from(hour).checked_mul(crate::HOUR).unwrap()
+ u64::from(minute).checked_mul(crate::MINUTE).unwrap()
+ u64::from(second);
let secs = if gmt_offset >= 0 {
secs.checked_sub(u64::try_from(gmt_offset).unwrap())
} else {
secs.checked_add(u64::try_from(gmt_offset.abs()).unwrap())
};
assert_eq!(duration.as_secs(), secs.unwrap());
}
Ok(())
}
#[test]
#[cfg(feature="unix")]
fn test_trying_times_from_durations() -> CrateResult<()> {
for (unix_seconds, year, month, day, hour, minute, second) in vec![
(0_u64, 1970_i64, Month::January, 1_u8, 0_u8, 0_u8, 0_u8),
(10, 1970, Month::January, 1, 0, 0, 10),
] {
let time = Time::try_from(Duration::from_secs(unix_seconds))?;
assert_eq!(time.year, year);
assert_eq!(time.month, month);
assert_eq!(time.day, day);
assert_eq!(time.hour, hour);
assert_eq!(time.minute, minute);
assert_eq!(time.second, second);
assert!(time.gmt_offset.is_none());
}
Ok(())
}
#[test]
fn test_time_ordering() -> CrateResult<()> {
for (
first_year, first_month, first_day, first_hour, first_minute, first_second, first_gmt_offset,
second_year, second_month, second_day, second_hour, second_minute, second_second, second_gmt_offset,
ordering,
) in vec![
(
2020_i64, Month::January, 1_u8, 0_u8, 1_u8, 59_u8, 0_i32,
2020_i64, Month::January, 1_u8, 0_u8, 1_u8, 59_u8, 0_i32,
Ordering::Equal,
),
(
2020_i64, Month::January, 1_u8, 0_u8, 2_u8, 59_u8, 60_i32,
2020_i64, Month::January, 1_u8, 0_u8, 0_u8, 59_u8, -60_i32,
Ordering::Equal,
),
(
2020_i64, Month::January, 1_u8, 0_u8, 2_u8, 59_u8, 0_i32,
2020_i64, Month::January, 1_u8, 0_u8, 0_u8, 59_u8, -60_i32,
Ordering::Greater,
),
(
2020_i64, Month::January, 1_u8, 0_u8, 2_u8, 59_u8, 60_i32,
2020_i64, Month::January, 1_u8, 0_u8, 0_u8, 59_u8, -120_i32,
Ordering::Less,
),
(
2020_i64, Month::January, 1_u8, 0_u8, 0_u8, 0_u8, 60_i32,
2019_i64, Month::December, 31_u8, 23_u8, 59_u8, 59_u8, 0_i32,
Ordering::Less,
),
(
2020_i64, Month::January, 1_u8, 0_u8, 0_u8, 0_u8, 0_i32,
2019_i64, Month::December, 31_u8, 23_u8, 59_u8, 59_u8, 0_i32,
Ordering::Greater,
),
(
2020_i64, Month::January, 1_u8, 0_u8, 1_u8, 0_u8, 60_i32,
2019_i64, Month::December, 31_u8, 23_u8, 59_u8, 0_u8, -60_i32,
Ordering::Equal,
),
] {
assert_eq!(
Time::make(first_year, first_month, first_day, first_hour, first_minute, first_second, Some(first_gmt_offset))?.cmp(
&Time::make(second_year, second_month, second_day, second_hour, second_minute, second_second, Some(second_gmt_offset))?
),
ordering,
);
}
Ok(())
}
#[derive(Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Clone, Copy)]
pub enum Month {
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December,
}
impl Month {
fn wrapping_next(&self) -> Self {
match self {
Month::January => Month::February,
Month::February => Month::March,
Month::March => Month::April,
Month::April => Month::May,
Month::May => Month::June,
Month::June => Month::July,
Month::July => Month::August,
Month::August => Month::September,
Month::September => Month::October,
Month::October => Month::November,
Month::November => Month::December,
Month::December => Month::January,
}
}
fn wrapping_last(&self) -> Self {
match self {
Month::January => Month::December,
Month::February => Month::January,
Month::March => Month::February,
Month::April => Month::March,
Month::May => Month::April,
Month::June => Month::May,
Month::July => Month::June,
Month::August => Month::July,
Month::September => Month::August,
Month::October => Month::September,
Month::November => Month::October,
Month::December => Month::November,
}
}
fn to_unix(&self) -> i32 {
match self {
Month::January => 0,
Month::February => 1,
Month::March => 2,
Month::April => 3,
Month::May => 4,
Month::June => 5,
Month::July => 6,
Month::August => 7,
Month::September => 8,
Month::October => 9,
Month::November => 10,
Month::December => 11,
}
}
#[cfg(feature="unix")]
fn try_from_unix(tm_mon: i32) -> CrateResult<Self> {
match tm_mon {
0 => Ok(Month::January),
1 => Ok(Month::February),
2 => Ok(Month::March),
3 => Ok(Month::April),
4 => Ok(Month::May),
5 => Ok(Month::June),
6 => Ok(Month::July),
7 => Ok(Month::August),
8 => Ok(Month::September),
9 => Ok(Month::October),
10 => Ok(Month::November),
11 => Ok(Month::December),
_ => Err(Error::from(__!("Invalid Unix month: {tm_mon}", tm_mon=tm_mon))),
}
}
}
impl Display for Month {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
f.write_str(match self {
Month::January => "January",
Month::February => "February",
Month::March => "March",
Month::April => "April",
Month::May => "May",
Month::June => "June",
Month::July => "July",
Month::August => "August",
Month::September => "September",
Month::October => "October",
Month::November => "November",
Month::December => "December",
})
}
}
fn last_day_of_month(month: &Month, leap_year: bool) -> u8 {
match month {
Month::January => 31,
Month::February => if leap_year { END_OF_FEBRUARY_IN_LEAP_YEARS } else { END_OF_FEBRUARY_IN_COMMON_YEARS },
Month::March => 31,
Month::April => 30,
Month::May => 31,
Month::June => 30,
Month::July => 31,
Month::August => 31,
Month::September => 30,
Month::October => 31,
Month::November => 30,
Month::December => 31,
}
}