use regex::Regex;
use serde::{Deserialize, Deserializer, Serializer};
use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
use std::{fmt::Display, num::ParseIntError};
use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset, macros::format_description};
lazy_static::lazy_static! {
static ref RE_ISO8601_DATETIME_OFFSET: Regex = Regex::new(r"^(?P<year>\d{4})-?(?P<month>\d{2})-?(?P<day>\d{2})(?P<prefix>[tT\s])(?P<hour>\d{1,2}):?(?P<minute>\d{2})?(:?(?P<second>\d{2})(\.?(?P<nano>\d{1,10}))?)?(?P<tz>[Z ]?([+-]\d{1,2}(?P<offset_colon>:?)(\d{2})?)?)?$").unwrap();
static ref RE_DATE_YMD: Regex = Regex::new(r#"^(?P<year>\d{4})-(?P<month>\d{1,2})?(-(?P<day>\d{1,2}))?$"#).unwrap();
static ref RE_DATE_YO: Regex = Regex::new(r#"^(?P<year>\d{4})-(?P<ordinal>\d{3})?$"#).unwrap();
static ref RE_DATE_YM: Regex = Regex::new(r#"^(?P<year>\d{4})-(?P<month>\d{1,2})?$"#).unwrap();
static ref RE_DATE_YEAR: Regex = Regex::new(r#"^(?P<year>\d{1,})$"#).unwrap();
static ref RE_TIME_HMS_OFFSET_MERIDAN: Regex = Regex::new(r#"^(?P<prefix>[\stT]?)?(?P<hour>\d{1,2})[ :]?(?P<minute>\d{2})?:?((?P<second>\d{2})(\.?(?P<nano>\d{1,10}))?)?:?\s?(?P<meridan>[apAP]\.?[mM]\.?)?\s?(?P<tz>[zZ]|[+-]\d{1,2}:?(\d{2})?)?$"#).unwrap();
static ref RE_TIME_OFFSET: Regex = Regex::new(r#"^(?P<tz>[zZ]|[+-]\d{1,2}(:?\d{2})?)$"#).unwrap();
static ref RE_OFFSET_RANGE: Regex = Regex::new(r"^(?P<hour>\d{1,2})(?P<offset_colon>:?)?(?P<minute>\d{2})?$").unwrap();
static ref RE_DURATION: Regex = Regex::new(r#"^P(?P<period>((?P<year>\d{1,2})[Yy])?-?(((?P<month>\d{1,2})[Mm])-?)?((?P<week>\d{1,2})[Ww])?-?((?P<day>\d{1,2})[dd])?)?T?(?P<time>((?P<hour>\d{1,2})[Hh]?):?((?P<minute>:?\d{1,2})[Mm]?)?:?((?P<second>\d{1,2})[Ss]?)?)?$"#).unwrap();
}
#[test]
fn regex_iso8601_datetime_offset() {
assert!(RE_ISO8601_DATETIME_OFFSET.is_match("2022-02-07T19:22:27+00:00"));
assert!(RE_ISO8601_DATETIME_OFFSET.is_match("2022-02-07T19:22:27.100+00:00"));
assert!(RE_ISO8601_DATETIME_OFFSET.is_match("2022-02-07T19:22:27Z"));
assert!(RE_ISO8601_DATETIME_OFFSET.is_match("20220207T192227Z"));
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("The offset {0:?} was invalid.")]
InvalidOffset(String),
#[error("The time {0:?} was invalid.")]
InvalidTime(String),
#[error("The date {0:?} was invalid.")]
InvalidDate(String),
#[error("The value {0:?} could not be parsed as a ISO8601/RFC3339 datetime string.")]
NonISO8601DateTime(String),
#[error("The value {0:?} could not be parsed as a RFC2282 datetime string.")]
NonRfc2822DateTime(String),
#[error(
"The value {0:#?} could not be parsed as a datetime, date, time or offset; making it completely invalid."
)]
CompletelyInvalid(String),
#[error("The time {0:?} is missing an hour value.")]
MissingHour(String),
#[error("The time {0:?} is missing an month value.")]
MissingMonth(String),
#[error("The time {0:?} is missing an dayvalue.")]
MissingDay(String),
#[error("The oridinal date {0:?} is missing an year value.")]
MissingYear(String),
#[error("The ordinal date {0:?} is missing an day value.")]
MissingOrdinalDay(String),
#[error("Could not parsed as an integer: {0:?}")]
Integer(#[from] ParseIntError),
#[error(transparent)]
Time(#[from] time::Error),
#[error("Missing a date value when converting this into a concrete datetime value.")]
MissingDate,
#[error("Missing a time value when converting this into a concrete datetime value.")]
MissingTime,
#[error("Missing an offset value when converting this into a concrete datetime value.")]
MissingOffset,
}
impl std::cmp::PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Offset {
pub data: time::UtcOffset,
with_minutes: bool,
with_colon: bool,
}
impl PartialOrd for Offset {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Offset {
fn cmp(&self, other: &Self) -> Ordering {
self.data.cmp(&other.data)
}
}
impl Display for Offset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let offset_str = if self.data.is_utc() {
"Z".to_string()
} else {
let hour = self.data.whole_hours();
let minute = self.data.minutes_past_hour().abs();
format!(
"{hour:+03}{}{}",
if self.with_colon { ":" } else { "" },
if minute == 0 && !self.with_minutes {
"".into()
} else {
format!("{minute:0>2}")
}
)
};
f.write_str(&offset_str)
}
}
impl FromStr for Offset {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let symbol_char = s.to_ascii_lowercase().chars().next();
let (offset_secs, with_minutes, with_colon) = s
.get(1..)
.and_then(|offset| RE_OFFSET_RANGE.captures(offset))
.map(|captures| {
let hour: i32 = captures
.name("hour")
.and_then(|v| v.as_str().parse().ok())
.unwrap_or_default();
let (minute, has_minutes): (i32, bool) = (
captures
.name("minute")
.and_then(|v| v.as_str().parse().ok())
.unwrap_or_default(),
captures.name("minute").is_some(),
);
let with_colon = captures
.name("offset_colon")
.filter(|m| !m.as_str().is_empty())
.is_some();
(hour * 60 * 60 + minute * 60, has_minutes, with_colon)
})
.unwrap_or_default();
if symbol_char == Some('z') {
time::UtcOffset::from_whole_seconds(0)
.map_err(|e| Self::Err::Time(e.into()))
.map(|data| Self {
data,
with_minutes,
with_colon,
})
} else if matches!(symbol_char, Some('-')) || matches!(symbol_char, Some('+')) {
time::UtcOffset::from_whole_seconds(
offset_secs
* if matches!(symbol_char, Some('+')) {
1
} else {
-1
},
)
.map_err(|e| Self::Err::Time(e.into()))
.map(|data| Self {
data,
with_minutes,
with_colon,
})
} else {
Err(Self::Err::InvalidOffset(s.to_string()))
}
}
}
impl Offset {
fn from_offset(data: time::UtcOffset) -> Self {
Self {
data,
with_minutes: true,
with_colon: false,
}
}
fn from_offset_with_colon(data: time::UtcOffset, with_colon: bool) -> Self {
Self {
data,
with_minutes: true,
with_colon,
}
}
}
impl From<time::UtcOffset> for Offset {
fn from(value: time::UtcOffset) -> Self {
Self::from_offset(value)
}
}
#[test]
fn offset_to_string() {
assert_eq!(
Offset::from_str("-0800").map(|o| o.to_string()),
Ok("-0800".to_owned())
);
assert_eq!(
Offset::from_str("-08:00").map(|o| o.to_string()),
Ok("-08:00".to_owned())
);
assert_eq!(
Offset::from_str("-08").map(|o| o.to_string()),
Ok("-08".to_owned())
);
assert_eq!(
Offset::from_str("+00").map(|o| o.to_string()),
Ok("Z".to_owned())
);
assert_eq!(
Offset::from_str("+00:00").map(|o| o.to_string()),
Ok("Z".to_owned())
);
assert_eq!(
Offset::from_str("-00:00").map(|o| o.to_string()),
Ok("Z".to_owned())
);
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
pub data: time::Time,
pub offset: Option<Offset>,
pub has_seconds: bool,
pub prefix: Option<char>,
}
impl Time {
pub fn with_offset(&self, offset: Option<Offset>) -> Self {
let mut other = self.clone();
other.offset = offset;
other
}
pub fn from_time(
data: time::Time,
has_seconds: bool,
prefix: Option<char>,
offset: Option<Offset>,
) -> Self {
Self {
data,
has_seconds,
prefix,
offset,
}
}
}
impl Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut format_items = vec![];
if let Some(prefix) = self.prefix {
format_items.push(prefix.to_string());
}
let hour = self.data.hour();
let minute = self.data.minute();
format_items.push(if self.has_seconds {
let second = self.data.second();
let subseconds = self.data.nanosecond();
let seconds = if subseconds != 0 {
format!("{second:02}.{subseconds:0>9}")
.trim_end_matches('0')
.to_string()
} else {
format!("{second:02}")
};
format!("{hour:02}:{minute:02}:{seconds}")
} else {
format!("{hour:02}:{minute:02}")
});
let time_string = format_items.join("");
let offset_string = self.offset.as_ref().map(|o| o.to_string());
f.write_str(&[time_string, offset_string.unwrap_or_default()].join(""))
}
}
#[test]
fn time_to_string() {
assert_eq!(
Time::from_str("1 PM").map(|t| t.to_string()),
Ok("13:00".to_owned())
);
assert_eq!(
Time::from_str("1:30:10.302 PM").map(|t| t.to_string()),
Ok("13:30:10.302".to_owned())
);
assert_eq!(
Time::from_str("T1 PM").map(|t| t.to_string()),
Ok("T13:00".to_owned())
);
assert_eq!(
Time::from_str(" 1 PM").map(|t| t.to_string()),
Ok(" 13:00".to_owned())
);
assert_eq!(
Time::from_str("March 14th 2013").map(|t| t.to_string()),
Err(Error::InvalidTime("March 14th 2013".to_string()))
);
}
impl FromStr for Time {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
RE_TIME_HMS_OFFSET_MERIDAN
.captures(s)
.ok_or_else(|| Self::Err::InvalidTime(s.to_string()))
.and_then(|params| {
let has_seconds = params.name("second").is_some();
let prefix = params
.name("prefix")
.and_then(|m| m.as_str().chars().next());
let mut hour = params
.name("hour")
.filter(|s| !s.is_empty())
.map(|m| m.as_str())
.ok_or_else(|| Self::Err::MissingHour(s.to_string()))?
.parse()
.map_err(Self::Err::Integer)?;
let min = params
.name("minute")
.filter(|s| !s.is_empty())
.map(|m| m.as_str())
.unwrap_or("0")
.parse()
.map_err(Self::Err::Integer)?;
let second = params
.name("second")
.filter(|s| !s.is_empty())
.map(|m| m.as_str())
.unwrap_or("0")
.parse()
.map_err(Self::Err::Integer)?;
let nano = params
.name("nano")
.filter(|s| !s.is_empty())
.map(|m| format!("{:0<9}", m.as_str().trim_start_matches('0')))
.unwrap_or_else(|| "0".to_string())
.parse()
.map_err(Self::Err::Integer)?;
let offset = if let Some(offset_str) = params.name("tz") {
Some(Offset::from_str(offset_str.as_str())?)
} else {
None
};
if Some("pm".to_string())
== params
.name("meridan")
.map(|m| m.as_str().to_ascii_lowercase().replace('.', ""))
&& hour < 12
{
hour += 12;
}
time::Time::from_hms_nano(hour, min, second, nano)
.map(|data| Self {
data,
offset,
has_seconds,
prefix,
})
.map_err(|_| Self::Err::InvalidTime(s.to_owned()))
})
}
}
impl From<time::Time> for Time {
fn from(data: time::Time) -> Self {
Self {
data,
offset: None,
prefix: None,
has_seconds: false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
pub data: time::Date,
pub ordinal: bool,
pub has_day: bool,
}
impl Date {
fn from_date(data: time::Date, ordinal: bool, has_day: bool) -> Self {
Self {
data,
ordinal,
has_day,
}
}
}
impl Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let year = self.data.year();
f.write_str(&if self.ordinal {
let oridinal_day = self.data.ordinal();
format!("{year:04}-{oridinal_day:03}")
} else if !self.has_day {
let month: u8 = self.data.month().into();
format!("{year:04}-{month:02}")
} else {
let month: u8 = self.data.month().into();
let day = self.data.day();
format!("{year:04}-{month:02}-{day:02}")
})
}
}
impl FromStr for Date {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let result = if let Some(parts) = RE_DATE_YO.captures(s) {
let year = parts
.name("year")
.and_then(|v| v.as_str().parse().ok())
.ok_or_else(|| Error::MissingYear(s.to_string()))?;
let ordinal = parts
.name("ordinal")
.and_then(|v| v.as_str().parse().ok())
.ok_or_else(|| Error::MissingOrdinalDay(s.to_string()))?;
time::Date::from_ordinal_date(year, ordinal)
.map(|d| (d, false))
.map_err(|e| Self::Err::Time(e.into()))
} else if let Some(parts) = RE_DATE_YMD.captures(s) {
parts
.name("year")
.ok_or_else(|| Error::MissingYear(s.to_string()))
.or_else(|_| {
parts
.name("month")
.ok_or_else(|| Error::MissingMonth(s.to_string()))
})?;
let has_day = parts.name("day").is_some();
let adjusted_s = if has_day {
s.to_string()
} else {
format!("{}-01", s)
};
time::Date::parse(&adjusted_s, format_description!("[year]-[month]-[day]"))
.map_err(|e| Self::Err::Time(e.into()))
.map(|d| (d, has_day))
} else if let Some(parts) = RE_DATE_YM.captures(s) {
parts
.name("year")
.ok_or_else(|| Error::MissingYear(s.to_string()))
.or_else(|_| {
parts
.name("month")
.ok_or_else(|| Error::MissingMonth(s.to_string()))
})?;
time::Date::parse(s, format_description!("[year]-[month]"))
.map_err(|e| Self::Err::Time(e.into()))
.map(|d| (d, false))
} else if let Some(parts) = RE_DATE_YEAR.captures(s) {
parts
.name("year")
.ok_or_else(|| Error::MissingYear(s.to_string()))?;
let new_date = time::Date::MIN
.replace_year(s.parse().map_err(|_| Error::MissingYear(s.to_string()))?)
.map_err(|_| Self::Err::MissingYear(s.to_string()))?;
Ok((new_date, false))
} else {
Err(Self::Err::InvalidDate(s.to_string()))
};
result.map(|(data, has_day)| Self {
data,
ordinal: RE_DATE_YO.is_match(s),
has_day,
})
}
}
impl<'de> Deserialize<'de> for Date {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).and_then(|stamp_string| {
Self::from_str(&stamp_string).map_err(serde::de::Error::custom)
})
}
}
impl From<time::Date> for Date {
fn from(data: time::Date) -> Self {
Self {
data,
ordinal: false,
has_day: true,
}
}
}
#[cfg(test)]
#[test]
fn date_to_string() {
assert_eq!(
Date::from_str("2013-034").map(|s| s.to_string()),
Ok("2013-034".to_owned())
);
assert_eq!(
Date::from_str("2013-03").map(|s| s.to_string()),
Ok("2013-03".to_owned())
);
assert_eq!(
Date::from_str("2013-03-01").map(|s| s.to_string()),
Ok("2013-03-01".to_owned())
);
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct Stamp {
pub time: Option<Time>,
pub date: Option<Date>,
pub offset: Option<Offset>,
was_iso8601: bool,
}
impl Display for Stamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let v = [
self.date.as_ref().map(|s| s.to_string()),
self.time
.as_ref()
.map(|t| {
if self.offset.is_some() {
t.with_offset(self.offset.clone())
} else {
t.clone()
}
})
.map(|t| t.to_string()),
]
.iter()
.filter_map(|o| o.clone())
.collect::<Vec<_>>()
.join("");
f.write_str(&v)
}
}
impl std::fmt::Debug for Stamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Stamp")
.field("value", &self.to_string())
.field("was_iso8601", &self.was_iso8601)
.finish()
}
}
impl Stamp {
pub fn new() -> Self {
Self {
time: None,
date: None,
offset: None,
was_iso8601: false,
}
}
pub fn from_iso8601(temporal_value: &str) -> Result<Self, Error> {
if let Some(components) = RE_ISO8601_DATETIME_OFFSET.captures(temporal_value) {
let prefix = components
.name("prefix")
.map(|p| p.as_str())
.and_then(|c| c.chars().next());
time::OffsetDateTime::parse(
temporal_value,
&time::format_description::well_known::Rfc3339,
)
.map_err(|e| Error::Time(e.into()))
.map(|dt| {
let with_colon = temporal_value.contains(":00")
|| temporal_value.contains(":+")
|| temporal_value.contains(":-")
|| temporal_value.contains(":30")
|| temporal_value.contains(":45");
Self {
date: Some(Date::from_date(dt.date(), false, true)),
time: Some(Time::from_time(dt.time(), true, prefix, None)).map(|mut t| {
t.prefix = prefix;
t
}),
offset: Some(Offset::from_offset_with_colon(dt.offset(), with_colon)),
was_iso8601: true,
}
})
.or_else(|_| {
let mut parts = temporal_value
.splitn(2, prefix.unwrap_or(' '))
.map(|s| s.to_owned());
let date_str = parts.next().unwrap();
let date = Date::from_str(&date_str)?;
let time_str = parts.next().unwrap();
let time = Time::from_str(&time_str).map(|mut t| {
t.prefix = prefix;
t
})?;
Ok(Stamp::compose(Some(date), Some(time), None))
})
} else {
Err(Error::NonISO8601DateTime(temporal_value.to_string()))
}
}
pub fn from_rfc2822(temporal_value: &str) -> Result<Self, Error> {
time::OffsetDateTime::parse(
temporal_value,
&time::format_description::well_known::Rfc2822,
)
.map(|dt| Self {
time: Some(dt.time().into()),
date: Some(dt.date().into()),
offset: Some(dt.offset().into()),
was_iso8601: true,
})
.map_err(|_| Error::NonRfc2822DateTime(temporal_value.to_string()))
}
pub fn parse(temporal_value: impl Into<String>) -> Result<Self, Error> {
let temporal_str_value = temporal_value.into();
let santizied_temporal_value = temporal_str_value.trim().to_string();
if let Ok(dt) = Self::from_rfc2822(&santizied_temporal_value) {
return Ok(dt);
} else if let Ok(dt) = Self::from_iso8601(&santizied_temporal_value) {
return Ok(dt);
}
let values = santizied_temporal_value
.split(' ')
.map(|t| t.to_string())
.collect::<Vec<_>>();
let iso = Self::from_iso8601(&santizied_temporal_value);
if iso.is_ok() {
iso
} else if RE_TIME_OFFSET.is_match(&santizied_temporal_value) {
Offset::from_str(&santizied_temporal_value).map(|offset| offset.into())
} else if RE_DATE_YO.is_match(&santizied_temporal_value)
|| RE_DATE_YMD.is_match(&santizied_temporal_value)
{
Date::from_str(&santizied_temporal_value).map(|date| date.into())
} else if RE_TIME_HMS_OFFSET_MERIDAN.is_match(&santizied_temporal_value) {
Time::from_str(&santizied_temporal_value).map(|time| time.into())
} else if values.len() != 1 {
let mut date = None;
let mut time = None;
let mut offset = None;
let parsed_values = values
.iter()
.cloned()
.flat_map(|v| Stamp::parse(&v).or_else(|_| Stamp::from_rfc2822(&v)).ok());
if let Some(stamp) = parsed_values.clone().find(|v| v.is_stamp()) {
Ok(stamp)
} else {
for value in parsed_values {
if value.is_date() && date.is_none() {
date = value.as_date()
} else if value.is_time() && time.is_none() {
time = value.as_time();
} else if value.is_offset() && offset.is_none() {
offset = value.as_offset();
}
}
time = time.map(|mut t| {
if t.prefix.is_none() {
t.prefix = Some(' ');
}
t
});
Ok(Stamp::compose(date, time, offset))
}
} else {
Err(Error::CompletelyInvalid(
santizied_temporal_value.to_string(),
))
}
}
pub fn is_date(&self) -> bool {
self.date.is_some() && self.time.is_none() && self.offset.is_none()
}
pub fn is_time(&self) -> bool {
self.date.is_none() && self.time.is_some()
}
pub fn is_offset(&self) -> bool {
self.date.is_none() && self.time.is_none() && self.offset.is_some()
}
pub fn as_date(&self) -> Option<Date> {
self.date.clone()
}
pub fn as_time(&self) -> Option<Time> {
self.time.clone()
}
pub fn as_offset(&self) -> Option<Offset> {
self.offset.clone()
}
pub fn compose(date: Option<Date>, time: Option<Time>, offset: Option<Offset>) -> Self {
Self {
date,
time,
offset,
was_iso8601: false,
}
}
pub fn now() -> Self {
time::OffsetDateTime::now_utc().into()
}
pub fn is_stamp(&self) -> bool {
self.date.is_some() && self.time.is_some()
}
pub fn is_empty(&self) -> bool {
self.date.is_none() && self.time.is_none() && self.offset.is_none()
}
}
impl std::default::Default for Stamp {
fn default() -> Self {
Self::now()
}
}
impl TryInto<OffsetDateTime> for Stamp {
type Error = Error;
fn try_into(self) -> Result<OffsetDateTime, Self::Error> {
let date = self.date.ok_or(Error::MissingDate)?;
let time = self.time.ok_or(Error::MissingTime)?;
let offset = self.offset.ok_or(Error::MissingOffset)?;
Ok(OffsetDateTime::new_in_offset(
date.data,
time.data,
offset.data,
))
}
}
impl FromStr for Stamp {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl From<Date> for Stamp {
fn from(value: Date) -> Self {
Self {
date: Some(value),
time: None,
offset: None,
was_iso8601: false,
}
}
}
impl From<Time> for Stamp {
fn from(value: Time) -> Self {
Self {
time: Some(value),
date: None,
offset: None,
was_iso8601: false,
}
}
}
impl From<Offset> for Stamp {
fn from(value: Offset) -> Self {
Self {
offset: Some(value),
date: None,
time: None,
was_iso8601: false,
}
}
}
impl From<time::OffsetDateTime> for Stamp {
fn from(dt: time::OffsetDateTime) -> Self {
Self {
time: Some(Time::from_time(dt.time(), true, Some('T'), None)),
date: Some(Date::from_date(dt.date(), false, true)),
offset: Some(Offset::from_offset_with_colon(dt.offset(), false)),
was_iso8601: true,
}
}
}
impl From<time::PrimitiveDateTime> for Stamp {
fn from(dt: time::PrimitiveDateTime) -> Self {
Self {
time: Some(Time::from_time(dt.time(), true, Some('T'), None)),
date: Some(Date::from_date(dt.date(), false, true)),
offset: Some(Offset::from_offset(UtcOffset::UTC)),
was_iso8601: true,
}
}
}
impl serde::Serialize for Stamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let stamp_string = self.to_string();
stamp_string.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Stamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).and_then(|stamp_string| {
Self::from_str(&stamp_string).map_err(serde::de::Error::custom)
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Duration {
pub year: Option<u32>,
pub month: Option<u32>,
pub week: Option<u32>,
pub day: Option<u32>,
pub time: Option<Time>,
}
impl FromStr for Duration {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(parts) = RE_DURATION.captures(s) {
let year = parts.name("year").and_then(|v| v.as_str().parse().ok());
let month = parts.name("month").and_then(|v| v.as_str().parse().ok());
let week = parts.name("week").and_then(|v| v.as_str().parse().ok());
let day = parts.name("day").and_then(|v| v.as_str().parse().ok());
let time = if parts.name("time").is_some() {
let hour = parts
.name("hour")
.and_then(|v| v.as_str().parse().ok())
.unwrap_or(0);
let minute = parts
.name("minute")
.and_then(|v| v.as_str().parse().ok())
.unwrap_or(0);
let second = parts
.name("second")
.and_then(|v| v.as_str().parse().ok())
.unwrap_or(0);
Some(Time::from_str(&format!("{}:{}:{}", hour, minute, second))?)
} else {
None
};
Ok(Self {
year,
month,
week,
day,
time,
})
} else {
Err(Error::CompletelyInvalid(s.to_string()))
}
}
}
impl Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut period = vec![];
let mut time = vec![];
if let Some(y) = self.year {
period.push(format!("{}Y", y));
}
if let Some(m) = self.month {
period.push(format!("{}M", m));
}
if let Some(w) = self.week {
period.push(format!("{}W", w));
}
if let Some(d) = self.day {
period.push(format!("{}D", d));
}
if let Some(t) = &self.time {
time.push(format!("{}H", t.data.hour()));
time.push(format!("{}M", t.data.minute()));
time.push(format!("{}S", t.data.second()));
}
f.write_fmt(format_args!("P{}{}", period.join(""), time.join("")))
}
}
impl serde::Serialize for Duration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)
.and_then(|s| Self::from_str(&s).map_err(|e| serde::de::Error::custom(Box::new(e))))
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Value {
Duration(Duration),
Timestamp(Stamp),
}
impl From<Duration> for Value {
fn from(duration: Duration) -> Self {
Self::Duration(duration)
}
}
impl From<Stamp> for Value {
fn from(stamp: Stamp) -> Self {
Self::Timestamp(stamp)
}
}
impl From<OffsetDateTime> for Value {
fn from(value: OffsetDateTime) -> Self {
Self::Timestamp(value.into())
}
}
impl From<PrimitiveDateTime> for Value {
fn from(value: PrimitiveDateTime) -> Self {
Self::Timestamp(value.into())
}
}
impl Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Duration(d) => f.write_fmt(format_args!("{}", d)),
Self::Timestamp(t) => f.write_fmt(format_args!("{}", t)),
}
}
}
impl FromStr for Value {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Duration::from_str(s)
.map(Self::Duration)
.or_else(|_| Stamp::from_str(s).map(Self::Timestamp))
}
}
impl serde::Serialize for Value {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let value_string = self.to_string();
value_string.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for Value {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value_string = String::deserialize(deserializer)?;
Self::from_str(&value_string).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
#[test]
fn value_to_string() {
assert_eq!(
Value::from_str("P2Y").map(|s| s.to_string()),
Ok("P2Y".to_string())
);
}
#[cfg(test)]
#[test]
fn stamp_to_string() {
assert_eq!(
Stamp::from_str("Mon, 16 May 2022 20:41:45 GMT").map(|s| s.to_string()),
Ok("2022-05-1620:41Z".to_owned()),
"format from issue #7"
);
assert_eq!(
Stamp::from_str("2000-10-01 1:00").map(|s| s.to_string()),
Ok("2000-10-01 01:00".to_owned())
);
assert_eq!(
Stamp::from_str("19:00:00-08:00").map(|s| s.to_string()),
Ok("19:00:00-08:00".to_owned())
);
assert_eq!(
Stamp::from_str("2000-10-01 19:00:00-0800").map(|s| s.to_string()),
Ok("2000-10-01 19:00:00-0800".to_owned())
);
assert_eq!(
Stamp::from_str("2009-06-26T19:00:00-08:00").map(|s| s.to_string()),
Ok("2009-06-26T19:00:00-08:00".to_owned())
);
assert_eq!(
Stamp::from_str("2009-06-26T19:00-08:00").map(|s| s.to_string()),
Ok("2009-06-26T19:00-08:00".to_owned()),
"preserving the colon in the output"
);
assert_eq!(
Stamp::from_str("2009-06-26T19:00-08").map(|s| s.to_string()),
Ok("2009-06-26T19:00-08".to_owned()),
"don't add the minutes in the offset"
);
assert_eq!(
Stamp::from_str("2009-06-26T19:00-0800").map(|s| s.to_string()),
Ok("2009-06-26T19:00-0800".to_owned()),
"no colon in offset, looking ISO8601-y"
);
assert_eq!(
Stamp::from_str("2009-06-26 19:00+0800").map(|s| s.to_string()),
Ok("2009-06-26 19:00+0800".to_owned()),
"maintain lack of colon in non-ISO8601 value"
);
}