#[cfg(feature = "chrono")]
mod chrono_impls {
use chrono::{DateTime, FixedOffset, NaiveDateTime, Utc};
use crate::{Timestamp, timestamp::TimestampError};
impl From<DateTime<Utc>> for Timestamp {
#[inline]
fn from(datetime: DateTime<Utc>) -> Self {
let mut ts = Self {
seconds: datetime.timestamp(),
nanos: datetime.timestamp_subsec_nanos().cast_signed(),
};
ts.normalize();
ts
}
}
impl From<NaiveDateTime> for Timestamp {
#[inline]
fn from(datetime: NaiveDateTime) -> Self {
let mut ts = Self {
seconds: datetime.and_utc().timestamp(),
nanos: datetime
.and_utc()
.timestamp_subsec_nanos()
.cast_signed(),
};
ts.normalize();
ts
}
}
impl TryFrom<Timestamp> for DateTime<Utc> {
type Error = TimestampError;
#[inline]
fn try_from(mut timestamp: Timestamp) -> Result<Self, Self::Error> {
timestamp.normalize();
u32::try_from(timestamp.nanos)
.ok()
.and_then(|nanos| Self::from_timestamp(timestamp.seconds, nanos))
.ok_or(TimestampError::OutOfSystemRange(timestamp))
}
}
impl TryFrom<Timestamp> for NaiveDateTime {
type Error = TimestampError;
#[inline]
fn try_from(mut timestamp: Timestamp) -> Result<Self, Self::Error> {
timestamp.normalize();
u32::try_from(timestamp.nanos)
.ok()
.and_then(|nanos| DateTime::<Utc>::from_timestamp(timestamp.seconds, nanos))
.map(|d| d.naive_local())
.ok_or(TimestampError::OutOfSystemRange(timestamp))
}
}
impl TryFrom<Timestamp> for DateTime<FixedOffset> {
type Error = TimestampError;
#[inline]
fn try_from(mut timestamp: Timestamp) -> Result<Self, Self::Error> {
timestamp.normalize();
let chrono_utc: DateTime<Utc> = timestamp.try_into()?;
Ok(chrono_utc.into())
}
}
impl TryFrom<chrono::DateTime<chrono::FixedOffset>> for Timestamp {
type Error = TimestampError;
#[inline]
fn try_from(dt: chrono::DateTime<chrono::FixedOffset>) -> Result<Self, Self::Error> {
let seconds = dt.timestamp();
let nanos = dt
.timestamp_subsec_nanos()
.try_into()
.map_err(|_| TimestampError::InvalidDateTime)?;
Ok(Self { seconds, nanos })
}
}
}
#[cfg(test)]
mod tests {
use crate::Timestamp;
use alloc::string::ToString;
use std::time::{SystemTime, UNIX_EPOCH};
fn ts(s: i64, n: i32) -> Timestamp {
Timestamp {
seconds: s,
nanos: n,
}
}
#[test]
fn test_system_time_epoch() {
let t: Timestamp = UNIX_EPOCH.into();
assert_eq!(t, ts(0, 0));
let sys: SystemTime = ts(0, 0).try_into().unwrap();
assert_eq!(sys, UNIX_EPOCH);
}
#[test]
fn test_system_time_roundtrip() {
let now = SystemTime::now();
let t: Timestamp = now.into();
let back: SystemTime = t
.try_into()
.expect("Timestamp should fit in SystemTime");
let diff = now
.duration_since(back)
.unwrap_or_else(|e| e.duration());
assert!(diff.as_nanos() < 1, "Roundtrip drifted significantly");
}
#[test]
fn test_system_time_pre_epoch() {
let pre_epoch = UNIX_EPOCH - core::time::Duration::from_secs(1);
let t: Timestamp = pre_epoch.into();
assert_eq!(t.seconds, -1);
let back: SystemTime = t.try_into().unwrap();
assert_eq!(back, pre_epoch);
}
#[test]
fn test_display_rfc3339() {
assert_eq!(ts(0, 0).to_string(), "1970-01-01T00:00:00Z");
assert_eq!(ts(0, 500_000_000).to_string(), "1970-01-01T00:00:00.5Z");
assert_eq!(ts(-1, 0).to_string(), "1969-12-31T23:59:59Z");
}
#[test]
fn test_from_str_rfc3339() {
use core::str::FromStr;
let t = Timestamp::from_str("1970-01-01T00:00:00Z").unwrap();
assert_eq!(t, ts(0, 0));
let t = Timestamp::from_str("1970-01-01T00:00:00.123456789Z").unwrap();
assert_eq!(t, ts(0, 123_456_789));
}
#[test]
fn test_string_roundtrip() {
let t = ts(1_600_000_000, 123_000_000);
let s = t.to_string();
let back: Timestamp = s.parse().unwrap();
assert_eq!(t, back);
}
#[cfg(feature = "chrono")]
mod chrono_tests {
use super::*;
use chrono::{NaiveDate, NaiveDateTime, TimeZone, Utc};
#[test]
fn test_chrono_utc_roundtrip() {
let dt = Utc
.with_ymd_and_hms(2024, 1, 1, 12, 0, 0)
.unwrap();
let t: Timestamp = dt.into();
assert_eq!(t.seconds, 1_704_110_400);
assert_eq!(t.nanos, 0);
let back: chrono::DateTime<Utc> = t.try_into().unwrap();
assert_eq!(dt, back);
}
#[test]
fn test_chrono_naive_utc_assumption() {
let naive = NaiveDate::from_ymd_opt(2024, 1, 1)
.unwrap()
.and_hms_opt(12, 0, 0)
.unwrap();
let t: Timestamp = naive.into();
assert_eq!(t.seconds, 1_704_110_400);
let back_naive: NaiveDateTime = t.try_into().unwrap();
assert_eq!(naive, back_naive);
}
#[test]
fn test_fixed_offset_conversions() {
use chrono::{FixedOffset, TimeZone};
let dt_offset = FixedOffset::east_opt(5 * 3600)
.unwrap()
.with_ymd_and_hms(2024, 1, 1, 12, 0, 0)
.unwrap();
let t: Timestamp = dt_offset
.try_into()
.expect("Should convert with normalization");
assert_eq!(t.seconds, 1_704_092_400);
}
}
}