dory/src/time/mod.rs

295 lines
7.4 KiB
Rust

//! 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<char> for Sign {
type Error = Error;
fn try_from(value: char) -> Result<Self, Self::Error> {
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<u8>,
}
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<u8>,
pub minute: Option<u8>,
pub second: Option<u8>,
pub tz: Option<TimeZone>,
}
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<std::cmp::Ordering> {
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<Self, Self::Err> {
Parser::new(s).parse()
}
}
impl DateTime {
pub fn normalize(&self) -> Option<Self> {
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",
]
)
}
}