use crate::{Error, ErrorKind, Result, Tag, Writer};
use core::{fmt, str::FromStr, time::Duration};
#[cfg(feature = "std")]
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "time")]
use time::PrimitiveDateTime;
const MIN_YEAR: u16 = 1970;
const MAX_UNIX_DURATION: Duration = Duration::from_secs(253_402_300_799);
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct DateTime {
year: u16,
month: u8,
day: u8,
hour: u8,
minutes: u8,
seconds: u8,
unix_duration: Duration,
}
impl DateTime {
#[allow(clippy::integer_arithmetic)]
pub fn new(year: u16, month: u8, day: u8, hour: u8, minutes: u8, seconds: u8) -> Result<Self> {
if year < MIN_YEAR
|| !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| !(0..=23).contains(&hour)
|| !(0..=59).contains(&minutes)
|| !(0..=59).contains(&seconds)
{
return Err(ErrorKind::DateTime.into());
}
let leap_years =
((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400;
let is_leap_year = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
let (mut ydays, mdays): (u16, u8) = match month {
1 => (0, 31),
2 if is_leap_year => (31, 29),
2 => (31, 28),
3 => (59, 31),
4 => (90, 30),
5 => (120, 31),
6 => (151, 30),
7 => (181, 31),
8 => (212, 31),
9 => (243, 30),
10 => (273, 31),
11 => (304, 30),
12 => (334, 31),
_ => return Err(ErrorKind::DateTime.into()),
};
if day > mdays || day == 0 {
return Err(ErrorKind::DateTime.into());
}
ydays += u16::from(day) - 1;
if is_leap_year && month > 2 {
ydays += 1;
}
let days = u64::from(year - 1970) * 365 + u64::from(leap_years) + u64::from(ydays);
let time = u64::from(seconds) + (u64::from(minutes) * 60) + (u64::from(hour) * 3600);
let unix_duration = Duration::from_secs(time + days * 86400);
if unix_duration > MAX_UNIX_DURATION {
return Err(ErrorKind::DateTime.into());
}
Ok(Self {
year,
month,
day,
hour,
minutes,
seconds,
unix_duration,
})
}
#[allow(clippy::integer_arithmetic)]
pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
if unix_duration > MAX_UNIX_DURATION {
return Err(ErrorKind::DateTime.into());
}
let secs_since_epoch = unix_duration.as_secs();
const LEAPOCH: i64 = 11017;
const DAYS_PER_400Y: i64 = 365 * 400 + 97;
const DAYS_PER_100Y: i64 = 365 * 100 + 24;
const DAYS_PER_4Y: i64 = 365 * 4 + 1;
let days = i64::try_from(secs_since_epoch / 86400)? - LEAPOCH;
let secs_of_day = secs_since_epoch % 86400;
let mut qc_cycles = days / DAYS_PER_400Y;
let mut remdays = days % DAYS_PER_400Y;
if remdays < 0 {
remdays += DAYS_PER_400Y;
qc_cycles -= 1;
}
let mut c_cycles = remdays / DAYS_PER_100Y;
if c_cycles == 4 {
c_cycles -= 1;
}
remdays -= c_cycles * DAYS_PER_100Y;
let mut q_cycles = remdays / DAYS_PER_4Y;
if q_cycles == 25 {
q_cycles -= 1;
}
remdays -= q_cycles * DAYS_PER_4Y;
let mut remyears = remdays / 365;
if remyears == 4 {
remyears -= 1;
}
remdays -= remyears * 365;
let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
let mut mon = 0;
for mon_len in months.iter() {
mon += 1;
if remdays < *mon_len {
break;
}
remdays -= *mon_len;
}
let mday = remdays + 1;
let mon = if mon + 2 > 12 {
year += 1;
mon - 10
} else {
mon + 2
};
let second = secs_of_day % 60;
let mins_of_day = secs_of_day / 60;
let minute = mins_of_day % 60;
let hour = mins_of_day / 60;
Self::new(
year.try_into()?,
mon,
mday.try_into()?,
hour.try_into()?,
minute.try_into()?,
second.try_into()?,
)
}
pub fn year(&self) -> u16 {
self.year
}
pub fn month(&self) -> u8 {
self.month
}
pub fn day(&self) -> u8 {
self.day
}
pub fn hour(&self) -> u8 {
self.hour
}
pub fn minutes(&self) -> u8 {
self.minutes
}
pub fn seconds(&self) -> u8 {
self.seconds
}
pub fn unix_duration(&self) -> Duration {
self.unix_duration
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn from_system_time(time: SystemTime) -> Result<Self> {
time.duration_since(UNIX_EPOCH)
.map_err(|_| ErrorKind::DateTime.into())
.and_then(Self::from_unix_duration)
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn to_system_time(&self) -> SystemTime {
UNIX_EPOCH + self.unix_duration()
}
}
impl FromStr for DateTime {
type Err = Error;
#[allow(clippy::integer_arithmetic)]
fn from_str(s: &str) -> Result<Self> {
match *s.as_bytes() {
[year1, year2, year3, year4, b'-', month1, month2, b'-', day1, day2, b'T', hour1, hour2, b':', min1, min2, b':', sec1, sec2, b'Z'] =>
{
let tag = Tag::GeneralizedTime;
let year =
u16::from(decode_decimal(tag, year1, year2).map_err(|_| ErrorKind::DateTime)?)
* 100
+ u16::from(
decode_decimal(tag, year3, year4).map_err(|_| ErrorKind::DateTime)?,
);
let month = decode_decimal(tag, month1, month2).map_err(|_| ErrorKind::DateTime)?;
let day = decode_decimal(tag, day1, day2).map_err(|_| ErrorKind::DateTime)?;
let hour = decode_decimal(tag, hour1, hour2).map_err(|_| ErrorKind::DateTime)?;
let minutes = decode_decimal(tag, min1, min2).map_err(|_| ErrorKind::DateTime)?;
let seconds = decode_decimal(tag, sec1, sec2).map_err(|_| ErrorKind::DateTime)?;
Self::new(year, month, day, hour, minutes, seconds)
}
_ => Err(ErrorKind::DateTime.into()),
}
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:02}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
self.year, self.month, self.day, self.hour, self.minutes, self.seconds
)
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl From<DateTime> for SystemTime {
fn from(time: DateTime) -> SystemTime {
time.to_system_time()
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl From<&DateTime> for SystemTime {
fn from(time: &DateTime) -> SystemTime {
time.to_system_time()
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl TryFrom<SystemTime> for DateTime {
type Error = Error;
fn try_from(time: SystemTime) -> Result<DateTime> {
DateTime::from_system_time(time)
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl TryFrom<&SystemTime> for DateTime {
type Error = Error;
fn try_from(time: &SystemTime) -> Result<DateTime> {
DateTime::from_system_time(*time)
}
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<DateTime> for PrimitiveDateTime {
type Error = Error;
fn try_from(time: DateTime) -> Result<PrimitiveDateTime> {
let month = (time.month() as u8).try_into()?;
let date = time::Date::from_calendar_date(i32::from(time.year()), month, time.day())?;
let time = time::Time::from_hms(time.hour(), time.minutes(), time.seconds())?;
Ok(PrimitiveDateTime::new(date, time))
}
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<PrimitiveDateTime> for DateTime {
type Error = Error;
fn try_from(time: PrimitiveDateTime) -> Result<DateTime> {
DateTime::new(
time.year().try_into().map_err(|_| ErrorKind::DateTime)?,
time.month().into(),
time.day(),
time.hour(),
time.minute(),
time.second(),
)
}
}
#[allow(clippy::integer_arithmetic)]
pub(crate) fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> {
if (b'0'..=b'9').contains(&hi) && (b'0'..=b'9').contains(&lo) {
Ok((hi - b'0') * 10 + (lo - b'0'))
} else {
Err(tag.value_error())
}
}
pub(crate) fn encode_decimal<W>(writer: &mut W, tag: Tag, value: u8) -> Result<()>
where
W: Writer + ?Sized,
{
let hi_val = value / 10;
if hi_val >= 10 {
return Err(tag.value_error());
}
writer.write_byte(b'0'.checked_add(hi_val).ok_or(ErrorKind::Overflow)?)?;
writer.write_byte(b'0'.checked_add(value % 10).ok_or(ErrorKind::Overflow)?)
}
#[cfg(test)]
mod tests {
use super::DateTime;
fn is_date_valid(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> bool {
DateTime::new(year, month, day, hour, minute, second).is_ok()
}
#[test]
fn feb_leap_year_handling() {
assert!(is_date_valid(2000, 2, 29, 0, 0, 0));
assert!(!is_date_valid(2001, 2, 29, 0, 0, 0));
assert!(!is_date_valid(2100, 2, 29, 0, 0, 0));
}
#[test]
fn from_str() {
let datetime = "2001-01-02T12:13:14Z".parse::<DateTime>().unwrap();
assert_eq!(datetime.year(), 2001);
assert_eq!(datetime.month(), 1);
assert_eq!(datetime.day(), 2);
assert_eq!(datetime.hour(), 12);
assert_eq!(datetime.minutes(), 13);
assert_eq!(datetime.seconds(), 14);
}
#[cfg(feature = "alloc")]
#[test]
fn display() {
use alloc::string::ToString;
let datetime = DateTime::new(2001, 01, 02, 12, 13, 14).unwrap();
assert_eq!(&datetime.to_string(), "2001-01-02T12:13:14Z");
}
}