use {
alloc::borrow::Cow,
core::{
cmp::Ordering,
convert::TryFrom,
fmt::{self, Debug, Display, Formatter},
time::Duration,
},
crate::{Error, Result as CrateResult},
};
#[cfg(feature="lib-c")]
use {
core::ptr,
};
#[cfg(any(feature="std", all(feature="std", feature="lib-c")))]
use {
std::time::{SystemTime, UNIX_EPOCH},
};
mod tests;
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;
const ONE_YEAR: i64 = (crate::DAY * 365) as i64;
const ONE_LEAP_YEAR: i64 = (crate::DAY * 366) as i64;
const SECONDS_OF_28_DAYS: i64 = (crate::DAY * 28) as i64;
const SECONDS_OF_29_DAYS: i64 = (crate::DAY * 29) as i64;
const SECONDS_OF_30_DAYS: i64 = (crate::DAY * 30) as i64;
const SECONDS_OF_31_DAYS: i64 = (crate::DAY * 31) as i64;
const SECONDS_OF_400_YEARS: i64 = 12_622_780_800;
const SECONDS_OF_1970_YEARS: i64 = 62_167_219_200;
#[derive(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> {
if day == 0 || match month {
Month::January | Month::March | Month::May | Month::July | Month::August | Month::October | Month::December => day > 31,
Month::April | Month::June | Month::September | Month::November => day > 30,
Month::February => 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 < &MIN_GMT_OFFSET || gmt_offset > &MAX_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="std")]
pub fn make_utc() -> CrateResult<Self> {
let (unix_seconds, is_positive) = match {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).map_err(|e| e.duration().as_secs())
} {
Ok(unix_seconds) => (unix_seconds, true),
Err(unix_seconds) => (unix_seconds, false),
};
let unix_seconds = i64::try_from(unix_seconds).map_err(|_| Error::from(__!("Failed to convert {} into i64", unix_seconds)))?;
try_unix_seconds_into_time(if is_positive { unix_seconds } else { -unix_seconds }, None)
}
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(all(feature="std", feature="lib-c"))]
pub fn make_local() -> CrateResult<Self> {
let gmt_offset = load_gmt_offset()?;
let (unix_seconds, is_positive) = match {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).map_err(|e| e.duration().as_secs())
} {
Ok(unix_seconds) => (unix_seconds, true),
Err(unix_seconds) => (unix_seconds, false),
};
let unix_seconds = i64::try_from(unix_seconds).map_err(|_| Error::from(__!("Failed to convert {} into i64", unix_seconds)))?;
try_unix_seconds_into_time(if is_positive { unix_seconds } else { -unix_seconds }, gmt_offset)
}
#[cfg(feature="lib-c")]
pub fn try_into_local(&self) -> CrateResult<Self> {
try_unix_seconds_into_time(self.try_into_unix_seconds()?, load_gmt_offset()?)
}
pub fn try_into_unix_seconds(&self) -> CrateResult<i64> {
if self.year >= 0 {
self.try_positive_time_into_unix_seconds()
} else {
self.try_negative_time_into_unix_seconds()
}
}
fn try_positive_time_into_unix_seconds(&self) -> CrateResult<i64> {
if self.year < 0 {
return Err(Error::from(__!("Year is negative")));
}
let mut unix_seconds = match (self.year / 400).checked_mul(SECONDS_OF_400_YEARS).map(|s|
s.checked_sub(SECONDS_OF_1970_YEARS).map(|mut s| {
for y in 0..self.year % 400 {
s = match s.checked_add(if is_leap_year(y) { ONE_LEAP_YEAR } else { ONE_YEAR }) {
Some(s) => s,
None => return None,
};
}
Some(s)
})
) {
Some(Some(Some(unix_seconds))) => unix_seconds,
_ => return Err(Error::from(__!("Year '{}' is too large", self.year)))?,
};
let mut month = Month::January;
let is_leap_year = is_leap_year(self.year);
loop {
if month < self.month {
unix_seconds = unix_seconds.checked_add(seconds_of_month(&month, is_leap_year))
.ok_or_else(|| Error::from(__!("Time is too large")))?;
month = month.wrapping_next();
} else {
break;
}
}
unix_seconds = match unix_seconds.checked_add((crate::DAY * u64::from(self.day - 1)) as i64).map(|s|
s.checked_add((crate::HOUR * u64::from(self.hour)) as i64).map(|s|
s.checked_add((crate::MINUTE * u64::from(self.minute)) as i64)
.map(|s| s.checked_add(self.second.into()))
)
) {
Some(Some(Some(Some(unix_seconds)))) => unix_seconds,
_ => return Err(Error::from(__!("Time is too large"))),
};
if let Some(gmt_offset) = self.gmt_offset {
unix_seconds = unix_seconds.checked_sub(gmt_offset.into()).ok_or_else(|| Error::from(__!("Time is too small")))?;
}
Ok(unix_seconds)
}
fn try_negative_time_into_unix_seconds(&self) -> CrateResult<i64> {
if self.year >= 0 {
return Err(Error::from(__!("Year is positive")));
}
let mut unix_seconds = match (self.year / 400).checked_mul(SECONDS_OF_400_YEARS).map(|s|
s.checked_sub(SECONDS_OF_1970_YEARS).map(|mut s| {
let remaining = (self.year % 400).abs();
match remaining {
0 => s.checked_add(ONE_LEAP_YEAR),
_ => {
for y in 1..remaining {
s = match s.checked_sub(if is_leap_year(y) { ONE_LEAP_YEAR } else { ONE_YEAR }) {
Some(s) => s,
None => return None,
};
}
Some(s)
},
}
})
) {
Some(Some(Some(unix_seconds))) => unix_seconds,
_ => return Err(Error::from(__!("Year '{}' is too small", self.year)))?,
};
let mut month = Month::December;
let is_leap_year = is_leap_year(self.year);
loop {
if month > self.month {
unix_seconds = unix_seconds.checked_sub(seconds_of_month(&month, is_leap_year))
.ok_or_else(|| Error::from(__!("Time is too small")))?;
month = month.wrapping_last();
} else {
break;
}
}
unix_seconds = match unix_seconds.checked_sub(
seconds_of_month(&self.month, is_leap_year) - (crate::DAY * u64::from(self.day)) as i64
).map(|s| {
let today_seconds = crate::HOUR * u64::from(self.hour) + crate::MINUTE * u64::from(self.minute) + u64::from(self.second);
s.checked_sub((crate::DAY - today_seconds) as i64)
}) {
Some(Some(unix_seconds)) => unix_seconds,
_ => return Err(Error::from(__!("Time is too small"))),
};
if let Some(gmt_offset) = self.gmt_offset {
unix_seconds = unix_seconds.checked_sub(gmt_offset.into()).ok_or_else(|| Error::from(__!("Time is too small")))?;
}
Ok(unix_seconds)
}
}
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 {
-(crate::DAY as i32)
} else {
return Ordering::Less;
},
1 => if self.day == 1 && last_day_of_month(&other.month, is_leap_year) == other.day {
crate::DAY as i32
} else {
return Ordering::Greater;
},
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)
}
}
impl TryFrom<&Time> for Duration {
type Error = Error;
fn try_from(time: &Time) -> Result<Self, Self::Error> {
let unix_seconds = time.try_into_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,
)))
}
}
impl TryFrom<Time> for Duration {
type Error = Error;
fn try_from(time: Time) -> Result<Self, Self::Error> {
Self::try_from(&time)
}
}
impl TryFrom<&Duration> for Time {
type Error = Error;
fn try_from(duration: &Duration) -> Result<Self, Self::Error> {
try_duration_into_time(duration, None)
}
}
impl TryFrom<Duration> for Time {
type Error = Error;
fn try_from(duration: Duration) -> Result<Self, Self::Error> {
Self::try_from(&duration)
}
}
#[cfg(feature="std")]
impl TryFrom<&SystemTime> for Time {
type Error = Error;
fn try_from(system_time: &SystemTime) -> Result<Self, Self::Error> {
match system_time.duration_since(UNIX_EPOCH) {
Ok(duration) => Self::try_from(duration),
Err(err) => Err(Error::from(__!(
"Failed calculating duration of {system_time:?} since UNIX Epoch: {err}",
system_time=system_time, err=err,
))),
}
}
}
#[cfg(feature="std")]
impl TryFrom<SystemTime> for Time {
type Error = Error;
fn try_from(system_time: SystemTime) -> Result<Self, Self::Error> {
Self::try_from(&system_time)
}
}
impl Debug for Time {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let month = match self.month {
Month::January => 1,
Month::February => 2,
Month::March => 3,
Month::April => 4,
Month::May => 5,
Month::June => 6,
Month::July => 7,
Month::August => 8,
Month::September => 9,
Month::October => 10,
Month::November => 11,
Month::December => 12,
};
let gmt_offset = match self.gmt_offset {
Some(gmt_offset) if gmt_offset != 0 => {
let hour = gmt_offset / crate::HOUR as i32;
let minute = ((gmt_offset - (hour * crate::HOUR as i32)) / crate::MINUTE as i32).abs();
Cow::Owned(format!(
"[{sign}{hour:02}:{minute:02}]",
sign=if gmt_offset >= 0 { concat!('+') } else { concat!() }, hour=hour, minute=minute,
))
},
_ => Cow::Borrowed("UTC"),
};
write!(
f,
"{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02} {gmt_offset}",
year=self.year, month=month, day=self.day, hour=self.hour, minute=self.minute, second=self.second, gmt_offset=gmt_offset,
)
}
}
#[cfg(feature="lib-c")]
fn load_gmt_offset() -> CrateResult<Option<i32>> {
match unsafe {
libc::time(ptr::null_mut())
} {
-1 => Err(Error::from(__!("libc::time() returned -1"))),
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(),
};
if unsafe {
libc::localtime_r(&time, &mut tm).is_null()
} {
return Err(Error::from(__!("libc::localtime_r() returned null")));
}
match tm.tm_gmtoff {
0 => Ok(None),
_ => Ok(Some(i32::try_from(tm.tm_gmtoff).map_err(|_| Error::from(__!("Invalid Unix GMT offset: {}", tm.tm_gmtoff)))?)),
}
},
}
}
pub fn try_unix_seconds_into_time(unix_seconds: i64, gmt_offset: Option<i32>) -> CrateResult<Time> {
let unix_seconds = match gmt_offset {
Some(gmt_offset) => unix_seconds.checked_add(gmt_offset.into()).ok_or_else(|| Error::from(__!(
"Failed adding GMT offset '{gmt_offset}' to Unix seconds '{unix_seconds}'", gmt_offset=gmt_offset, unix_seconds=unix_seconds,
)))?,
None => unix_seconds,
};
fn try_positive(all_seconds: i64, gmt_offset: Option<i32>) -> CrateResult<Time> {
let mut year = all_seconds / SECONDS_OF_400_YEARS * 400;
let mut all_seconds = all_seconds % SECONDS_OF_400_YEARS;
loop {
let one_year = if is_leap_year(year) { ONE_LEAP_YEAR } else { ONE_YEAR };
match all_seconds.checked_sub(one_year) {
Some(new_secs) if new_secs >= 0 => {
year += 1;
all_seconds = new_secs;
},
_ => break,
};
}
let (month, day, hour, minute, second) = month_day_hour_minute_second(is_leap_year(year), all_seconds);
Time::make(year, month, day, hour, minute, second, gmt_offset)
}
fn try_negative(all_seconds: i64, gmt_offset: Option<i32>) -> CrateResult<Time> {
let mut year = (all_seconds / SECONDS_OF_400_YEARS * 400).checked_sub(1).ok_or_else(|| Error::from(__!("Failed to calculating year")))?;
let mut all_seconds = all_seconds % SECONDS_OF_400_YEARS;
loop {
let one_year = if is_leap_year(year) { ONE_LEAP_YEAR } else { ONE_YEAR };
match all_seconds.checked_add(one_year) {
Some(new_secs) if new_secs <= 0 => {
year -= 1;
all_seconds = new_secs;
},
_ => break,
};
}
let is_leap_year = is_leap_year(year);
all_seconds += if is_leap_year { ONE_LEAP_YEAR } else { ONE_YEAR };
let (month, day, hour, minute, second) = month_day_hour_minute_second(is_leap_year, all_seconds);
Time::make(year, month, day, hour, minute, second, gmt_offset)
}
fn month_day_hour_minute_second(is_leap_year: bool, mut all_seconds: i64) -> (Month, u8, u8, u8, u8) {
let mut month = Month::January;
loop {
match all_seconds.checked_sub(seconds_of_month(&month, is_leap_year)) {
Some(new_secs) if new_secs >= 0 => {
month = month.wrapping_next();
all_seconds = new_secs;
},
_ => break,
};
}
let day = ((all_seconds / crate::DAY as i64) + 1) as u8;
let all_seconds = all_seconds % crate::DAY as i64;
let hour = (all_seconds / crate::HOUR as i64) as u8;
let all_seconds = all_seconds % crate::HOUR as i64;
let minute = (all_seconds / crate::MINUTE as i64) as u8;
let second = (all_seconds % crate::MINUTE as i64) as u8;
(month, day, hour, minute, second)
}
let all_seconds = unix_seconds.checked_add(SECONDS_OF_1970_YEARS)
.ok_or_else(|| Error::from(__!("Failed transforming Unix seconds: {}", unix_seconds)))?;
if all_seconds >= 0 {
try_positive(all_seconds, gmt_offset)
} else {
try_negative(all_seconds, gmt_offset)
}
}
fn try_duration_into_time(duration: &Duration, gmt_offset: Option<i32>) -> 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, gmt_offset)
}
#[cfg(feature="lib-c")]
pub fn try_duration_into_local_time(duration: &Duration) -> CrateResult<Time> {
try_duration_into_time(duration, load_gmt_offset()?)
}
fn is_leap_year(year: i64) -> bool {
match year % 4 {
0 => match year % 100 {
0 => year % 400 == 0,
_ => true,
}
_ => false,
}
}
#[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 {
pub fn order(&self) -> u8 {
match self {
Month::January => 1,
Month::February => 2,
Month::March => 3,
Month::April => 4,
Month::May => 5,
Month::June => 6,
Month::July => 7,
Month::August => 8,
Month::September => 9,
Month::October => 10,
Month::November => 11,
Month::December => 12,
}
}
pub fn try_from_order(order: u8) -> CrateResult<Self> {
match order {
1 => Ok(Month::January),
2 => Ok(Month::February),
3 => Ok(Month::March),
4 => Ok(Month::April),
5 => Ok(Month::May),
6 => Ok(Month::June),
7 => Ok(Month::July),
8 => Ok(Month::August),
9 => Ok(Month::September),
10 => Ok(Month::October),
11 => Ok(Month::November),
12 => Ok(Month::December),
_ => Err(Error::from(__!("Invalid month: {}", order))),
}
}
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(test)]
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,
}
}
fn seconds_of_month(month: &Month, leap_year: bool) -> i64 {
match month {
Month::January | Month::March | Month::May | Month::July | Month::August | Month::October | Month::December => SECONDS_OF_31_DAYS,
Month::February => if leap_year { SECONDS_OF_29_DAYS } else { SECONDS_OF_28_DAYS },
Month::April | Month::June | Month::September | Month::November => SECONDS_OF_30_DAYS,
}
}