//! A container representing a moment in ISO-8601 format Time use std::{cmp::Ordering, fmt, str::FromStr}; mod error; mod parser; pub use {error::Error, parser::Parser}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use x509_parser::der_parser::ber::ber_read_element_header; #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub enum Sign { Positive, Negative, } impl fmt::Display for Sign { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Positive => write!(f, "+"), Self::Negative => write!(f, "-"), } } } impl TryFrom for Sign { type Error = Error; fn try_from(value: char) -> Result { match value { '+' => Ok(Self::Positive), '-' => Ok(Self::Negative), _ => Err(Error::InvalidOffset), } } } #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct Offset { pub sign: Sign, pub hours: u8, pub minutes: Option, } impl fmt::Display for Offset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(m) = self.minutes { write!(f, "{}{:02}:{m:02}", self.sign, self.hours) } else { write!(f, "{}{:02}", self.sign, self.hours) } } } #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub enum TimeZone { UTC, Offset(Offset), } impl fmt::Display for TimeZone { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UTC => write!(f, "Z"), Self::Offset(o) => write!(f, "{o}"), } } } #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct DateTime { pub year: u32, pub month: u8, pub day: u8, pub hour: Option, pub minute: Option, pub second: Option, pub tz: Option, } impl fmt::Display for DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:04}", self.year)?; 'blk: { for field in &[self.month, self.day] { write!(f, "-{field:02}")?; } if let Some(h) = self.hour { write!(f, "T{h:02}")?; } else { break 'blk; } for field in &[self.minute, self.second] { if let Some(field) = field { write!(f, ":{field:02}")?; } else { break 'blk; } } } if let Some(ref tz) = self.tz { write!(f, "{tz}") } else { Ok(()) } } } impl PartialOrd for DateTime { fn partial_cmp(&self, other: &Self) -> Option { let a = self.normalize()?; let b = other.normalize()?; (a.year, a.month, a.day, a.hour, a.minute, a.second) .partial_cmp(&(b.year, b.month, b.day, b.hour, b.minute, b.second)) } } impl FromStr for DateTime { type Err = Error; fn from_str(s: &str) -> Result { Parser::new(s).parse() } } impl DateTime { pub fn normalize(&self) -> Option { if self.tz == Some(TimeZone::UTC) { Some(*self) } else { if let Some(TimeZone::Offset(tz)) = self.tz { let hour = if let Some(hour) = self.hour { match tz.sign { Sign::Positive => Some(hour + tz.hours), Sign::Negative => Some(hour - tz.hours), } } else { None }; let minute = 'blk: { if let Some(minutes) = self.minute { if let Some(o) = tz.minutes { match tz.sign { Sign::Positive => break 'blk Some(minutes + o), Sign::Negative => break 'blk Some(minutes - o), } } } None }; Some(DateTime { hour, minute, ..*self }) } else { None } } } } #[cfg(test)] mod test { use super::*; #[test] fn fmt_full() { let dt = DateTime { year: 2023, month: 5, day: 7, hour: Some(9), minute: Some(3), second: Some(8), tz: Some(TimeZone::Offset(Offset { sign: Sign::Positive, hours: 5, minutes: Some(15), })), }; assert_eq!(dt.to_string(), "2023-05-07T09:03:08+05:15"); } #[test] fn fmt_partial_offset() { let dt = DateTime { year: 2023, month: 5, day: 7, hour: Some(9), minute: Some(3), second: Some(8), tz: Some(TimeZone::Offset(Offset { sign: Sign::Negative, hours: 5, minutes: None, })), }; assert_eq!(dt.to_string(), "2023-05-07T09:03:08-05"); } #[test] fn fmt_utc() { let dt = DateTime { year: 2023, month: 5, day: 7, hour: Some(9), minute: Some(3), second: Some(8), tz: Some(TimeZone::UTC), }; assert_eq!(dt.to_string(), "2023-05-07T09:03:08Z"); } #[test] fn fmt_no_tz() { let dt = DateTime { year: 2023, month: 5, day: 7, hour: Some(9), minute: Some(3), second: Some(8), tz: None, }; assert_eq!(dt.to_string(), "2023-05-07T09:03:08"); } #[test] fn fmt_no_seconds() { let dt = DateTime { year: 2023, month: 5, day: 7, hour: Some(9), minute: Some(3), second: None, tz: Some(TimeZone::UTC), }; assert_eq!(dt.to_string(), "2023-05-07T09:03Z"); } #[test] fn fmt_no_minutes() { let dt = DateTime { year: 2023, month: 5, day: 7, hour: Some(9), minute: None, second: Some(50), tz: Some(TimeZone::UTC), }; assert_eq!(dt.to_string(), "2023-05-07T09Z"); } #[test] fn fmt_no_time() { let dt = DateTime { year: 2023, month: 5, day: 7, hour: None, minute: None, second: Some(50), tz: Some(TimeZone::UTC), }; assert_eq!(dt.to_string(), "2023-05-07Z"); } #[test] fn ordering() { let mut dates = vec![ "2023-06-10T10:01:52Z", "2023-06-10T08:01:52+05", "2023-06-10T11:30:30-05:15", ]; dates.sort_by(|a, b| a.partial_cmp(b).unwrap()); assert_eq!( dates, vec![ "2023-06-10T08:01:52+05", "2023-06-10T10:01:52Z", "2023-06-10T11:30:30-05:15", ] ) } }