use chrono::{FixedOffset, Offset, TimeZone, Timelike};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::str::FromStr;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct DateTime(chrono::DateTime<FixedOffset>);
impl FromStr for DateTime {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.ends_with('Z') {
FixedOffset::west(0).datetime_from_str(s, "%Y-%m-%dT%H:%M:%S%.fZ")
} else {
chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f%:z")
}
.map(Self::from)
}
}
impl std::fmt::Display for DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.0.offset().local_minus_utc() == 0 {
self.0.format("%Y-%m-%dT%H:%M:%S-00:00").fmt(f)
} else {
self.0.format("%Y-%m-%dT%H:%M:%S%:z").fmt(f)
}
}
}
impl Serialize for DateTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for DateTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str = <std::borrow::Cow<str>>::deserialize(deserializer)?;
let dt = str.parse().map_err(serde::de::Error::custom)?;
Ok(Self(dt))
}
}
impl From<DateTime> for chrono::DateTime<FixedOffset> {
fn from(dt: DateTime) -> Self {
dt.0
}
}
impl<Tz: chrono::TimeZone> From<chrono::DateTime<Tz>> for DateTime {
fn from(dt: chrono::DateTime<Tz>) -> Self {
Self(
dt.with_timezone(&dt.offset().fix())
.with_nanosecond(0)
.expect("timestamp must be valid with zero nanoseconds"),
)
}
}
impl<Tz: chrono::TimeZone> PartialEq<chrono::DateTime<Tz>> for DateTime {
fn eq(&self, other: &chrono::DateTime<Tz>) -> bool {
self.0.eq(&other.with_nanosecond(0).unwrap())
}
}
impl<Tz: chrono::TimeZone> PartialEq<DateTime> for chrono::DateTime<Tz> {
fn eq(&self, other: &DateTime) -> bool {
self.with_nanosecond(0).unwrap().eq(&other.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn parse() {
assert_eq!(
"2002-05-24T16:49:00-07:00".parse(),
Ok(DateTime(
FixedOffset::west(7 * 3600)
.ymd(2002, 5, 24)
.and_hms(16, 49, 0)
))
);
assert!(DateTime::from_str("2002-05-24T16:49:00").is_err());
}
#[test]
fn parse_utc() {
let reference = DateTime::from_str("2002-05-24T16:49:00-00:00").unwrap();
assert_eq!(
DateTime::from_str("2002-05-24T16:49:00+00:00").unwrap(),
reference
);
assert_eq!(
DateTime::from_str("2002-05-24T16:49:00Z").unwrap(),
reference
);
}
#[test]
fn to_string() {
assert_eq!(
DateTime(
FixedOffset::west(7 * 3600)
.ymd(2002, 5, 24)
.and_hms(16, 49, 0)
)
.to_string(),
"2002-05-24T16:49:00-07:00"
);
assert_eq!(
DateTime(FixedOffset::west(0).ymd(2002, 5, 24).and_hms(16, 49, 0)).to_string(),
"2002-05-24T16:49:00-00:00"
);
}
#[test]
fn conversions() {
assert_eq!(
chrono::DateTime::from(DateTime::from_str("2002-05-24T16:49:00-00:00").unwrap()),
FixedOffset::west(0).ymd(2002, 5, 24).and_hms(16, 49, 0),
);
assert_eq!(
DateTime::from(
FixedOffset::west(7 * 3600)
.ymd(2002, 5, 24)
.and_hms(16, 49, 0)
),
DateTime::from_str("2002-05-24T16:49:00-07:00").unwrap()
);
assert_eq!(
DateTime::from(chrono::Utc.ymd(2002, 5, 24).and_hms(16, 49, 0)),
DateTime::from_str("2002-05-24T16:49:00-00:00").unwrap()
);
}
}