use {
alloc::borrow::Cow,
core::{
cmp::Ordering,
fmt::{self, Debug, Formatter},
time::Duration,
},
crate::{Error, Month, Result as CrateResult, Weekday, month},
};
#[cfg(feature="lib-c")]
use core::ptr;
#[cfg(any(feature="std", all(feature="std", feature="lib-c")))]
use std::time::{SystemTime, UNIX_EPOCH};
pub type Year = i64;
pub type Day = u8;
pub type Hour = u8;
pub type Minute = u8;
pub type Second = u8;
pub type UnixSecond = i64;
pub type GmtOffset = i32;
mod tests;
const MAX_GMT_OFFSET: GmtOffset = 14 * 3600;
const MIN_GMT_OFFSET: GmtOffset = -12 * 3600;
const ONE_YEAR: UnixSecond = (crate::DAY * 365) as UnixSecond;
const ONE_LEAP_YEAR: UnixSecond = (crate::DAY * 366) as UnixSecond;
const SECONDS_OF_400_YEARS: UnixSecond = 12_622_780_800;
const SECONDS_OF_1970_YEARS: UnixSecond = 62_167_219_200;
#[derive(Eq, Hash, PartialOrd, Clone)]
pub struct Time {
year: Year,
month: Month,
day: Day,
weekday: Weekday,
hour: Hour,
minute: Minute,
second: Second,
gmt_offset: Option<GmtOffset>,
}
impl Time {
pub fn make(year: Year, month: Month, day: Day, hour: Hour, minute: Minute, second: Second, gmt_offset: Option<GmtOffset>)
-> 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) {
month::END_OF_FEBRUARY_IN_LEAP_YEARS
} else {
month::END_OF_FEBRUARY_IN_COMMON_YEARS
},
} {
return Err(err!("Invalid day: {day} of {month}", day=day, month=month));
}
if hour > 23 {
return Err(err!("Invalid hour: {hour}", hour=hour));
}
if minute > 59 {
return Err(err!("Invalid minute: {minute}", minute=minute));
}
if second > 59 {
return Err(err!("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(err!("Invalid GMT offset: {gmt_offset}", gmt_offset=gmt_offset));
}
}
let mut result = Self {
year, month, day,
weekday: Weekday::Monday,
hour, minute, second,
gmt_offset,
};
result.weekday = {
let tmp = result.try_into_unix_seconds()?
.checked_add(gmt_offset.unwrap_or(0).into()).ok_or_else(|| err!())?
.checked_abs().ok_or_else(|| err!())?;
Weekday::try_from_unix((tmp / i64::try_from(crate::DAY).map_err(|_| err!())? + 4) % 7)?
};
Ok(result)
}
pub const fn year(&self) -> Year {
self.year
}
pub const fn month(&self) -> Month {
self.month
}
pub const fn day(&self) -> Day {
self.day
}
pub const fn weekday(&self) -> Weekday {
self.weekday
}
pub const fn hour(&self) -> Hour {
self.hour
}
pub const fn minute(&self) -> Minute {
self.minute
}
pub const fn second(&self) -> Second {
self.second
}
pub const fn gmt_offset(&self) -> Option<GmtOffset> {
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 = UnixSecond::try_from(unix_seconds).map_err(|_| err!("Failed to convert {} into UnixSecond", 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 GmtOffset;
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(|| err!("Failed to subtract '{year}' by 1", year=self.year))?,
_ => self.year,
};
let month = self.month.wrapping_last();
let day = month::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 GmtOffset {
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 == month::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(|| err!("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 Hour, minute as Minute, second as Second, 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 = UnixSecond::try_from(unix_seconds).map_err(|_| err!("Failed to convert {} into UnixSecond", 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<UnixSecond> {
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<UnixSecond> {
if self.year < 0 {
return Err(err!("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(err!("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(month::seconds_of_month(&month, is_leap_year))
.ok_or_else(|| err!("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 UnixSecond).map(|s|
s.checked_add((crate::HOUR * u64::from(self.hour)) as UnixSecond).map(|s|
s.checked_add((crate::MINUTE * u64::from(self.minute)) as UnixSecond)
.map(|s| s.checked_add(self.second.into()))
)
) {
Some(Some(Some(Some(unix_seconds)))) => unix_seconds,
_ => return Err(err!("Time is too large")),
};
if let Some(gmt_offset) = self.gmt_offset {
unix_seconds = unix_seconds.checked_sub(gmt_offset.into()).ok_or_else(|| err!("Time is too small"))?;
}
Ok(unix_seconds)
}
fn try_negative_time_into_unix_seconds(&self) -> CrateResult<UnixSecond> {
if self.year >= 0 {
return Err(err!("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(err!("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(month::seconds_of_month(&month, is_leap_year))
.ok_or_else(|| err!("Time is too small"))?;
month = month.wrapping_last();
} else {
break;
}
}
unix_seconds = match unix_seconds.checked_sub(
month::seconds_of_month(&self.month, is_leap_year) - (crate::DAY * u64::from(self.day)) as UnixSecond
).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 UnixSecond)
}) {
Some(Some(unix_seconds)) => unix_seconds,
_ => return Err(err!("Time is too small")),
};
if let Some(gmt_offset) = self.gmt_offset {
unix_seconds = unix_seconds.checked_sub(gmt_offset.into()).ok_or_else(|| err!("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 as GmtOffset,
-1 => -(crate::DAY as GmtOffset),
1 => crate::DAY as GmtOffset,
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 month::last_day_of_month(&self.month, is_leap_year) == self.day && other.day == 1 {
-(crate::DAY as GmtOffset)
} else {
return Ordering::Less;
},
1 => if self.day == 1 && month::last_day_of_month(&other.month, is_leap_year) == other.day {
crate::DAY as GmtOffset
} 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 GmtOffset,
_ => return Ordering::Greater,
},
Some(-1) => match (self.month, self.day, other.month, other.day) {
(Month::December, 31, Month::January, 1) => -(crate::DAY as GmtOffset),
_ => 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 GmtOffset
- 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 GmtOffset
- 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(|_| err!(
"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(err!(
"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 GmtOffset;
let minute = ((gmt_offset - (hour * crate::HOUR as GmtOffset)) / crate::MINUTE as GmtOffset).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<GmtOffset>> {
match unsafe {
libc::time(ptr::null_mut())
} {
-1 => Err(err!("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,
#[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))]
tm_zone: ptr::null_mut(),
#[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd")))]
tm_zone: ptr::null(),
};
if unsafe {
libc::localtime_r(&time, &mut tm).is_null()
} {
return Err(err!("libc::localtime_r() returned null"));
}
match tm.tm_gmtoff {
0 => Ok(None),
_ => Ok(Some(GmtOffset::try_from(tm.tm_gmtoff).map_err(|_| err!("Invalid Unix GMT offset: {}", tm.tm_gmtoff))?)),
}
},
}
}
pub fn try_unix_seconds_into_time(unix_seconds: UnixSecond, gmt_offset: Option<GmtOffset>) -> CrateResult<Time> {
let unix_seconds = match gmt_offset {
Some(gmt_offset) => unix_seconds.checked_add(gmt_offset.into()).ok_or_else(|| err!(
"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: UnixSecond, gmt_offset: Option<GmtOffset>) -> 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: UnixSecond, gmt_offset: Option<GmtOffset>) -> CrateResult<Time> {
let mut year = (all_seconds / SECONDS_OF_400_YEARS * 400).checked_sub(1).ok_or_else(|| err!("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: UnixSecond) -> (Month, Day, Hour, Minute, Second) {
let mut month = Month::January;
loop {
match all_seconds.checked_sub(month::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 UnixSecond) + 1) as Day;
let all_seconds = all_seconds % crate::DAY as UnixSecond;
let hour = (all_seconds / crate::HOUR as UnixSecond) as Hour;
let all_seconds = all_seconds % crate::HOUR as UnixSecond;
let minute = (all_seconds / crate::MINUTE as UnixSecond) as Minute;
let second = (all_seconds % crate::MINUTE as UnixSecond) as Second;
(month, day, hour, minute, second)
}
let all_seconds = unix_seconds.checked_add(SECONDS_OF_1970_YEARS)
.ok_or_else(|| err!("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<GmtOffset>) -> CrateResult<Time> {
let unix_seconds = duration.as_secs();
let unix_seconds = UnixSecond::try_from(unix_seconds)
.map_err(|_| err!("Failed to convert {} into UnixSecond", 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: Year) -> bool {
match year % 4 {
0 => match year % 100 {
0 => year % 400 == 0,
_ => true,
}
_ => false,
}
}