Skip to main content

t2t_core/interop/
chrono.rs

1//! [`Timestamp`] and [`Timedelta`] to and from chrono's `DateTime` and `TimeDelta`.
2
3use chrono::{DateTime, TimeDelta, TimeZone, Utc};
4
5use crate::{OutOfRangeError, Timedelta, Timestamp};
6
7/// Every timestamp, in UTC, since chrono's reach years -262143 to 262142.
8impl From<Timestamp> for DateTime<Utc> {
9    #[inline]
10    fn from(instant: Timestamp) -> Self {
11        Self::from_timestamp_nanos(instant.0)
12    }
13}
14
15/// Refuses an instant outside [`Timestamp::MIN`] to [`Timestamp::MAX`], in any zone.
16impl<Z: TimeZone> TryFrom<DateTime<Z>> for Timestamp {
17    type Error = OutOfRangeError;
18
19    #[inline]
20    fn try_from(instant: DateTime<Z>) -> Result<Self, Self::Error> {
21        instant.timestamp_nanos_opt().map(Self).ok_or(OutOfRangeError)
22    }
23}
24
25/// Every span, since a `TimeDelta` holds every `i64` of nanoseconds.
26impl From<Timedelta> for TimeDelta {
27    #[inline]
28    fn from(span: Timedelta) -> Self {
29        Self::nanoseconds(span.0)
30    }
31}
32
33/// Refuses a duration past [`Timedelta::MIN`] or [`Timedelta::MAX`].
34impl TryFrom<TimeDelta> for Timedelta {
35    type Error = OutOfRangeError;
36
37    #[inline]
38    fn try_from(span: TimeDelta) -> Result<Self, Self::Error> {
39        span.num_nanoseconds().map(Self).ok_or(OutOfRangeError)
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use chrono::{DateTime, FixedOffset, TimeDelta, Utc};
46    use rstest::rstest;
47
48    use crate::{OutOfRangeError, Timedelta, Timestamp};
49
50    #[rstest]
51    #[case::the_earliest(Timestamp::MIN)]
52    #[case::before_the_epoch(Timestamp::from_nanos(-1))]
53    #[case::a_capture(Timestamp::from_nanos(1_789_544_735_123_456_789))]
54    #[case::the_latest(Timestamp::MAX)]
55    fn an_instant_crosses_both_ways(#[case] instant: Timestamp) {
56        let theirs = DateTime::<Utc>::from(instant);
57        assert_eq!(theirs.timestamp_nanos_opt(), Some(instant.as_nanos()), "the same instant");
58        assert_eq!(Timestamp::try_from(theirs), Ok(instant), "and back");
59    }
60
61    #[test]
62    fn an_instant_in_another_zone_is_the_same_instant() {
63        let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
64        let zone = FixedOffset::east_opt(18_000).expect("five hours east is an offset");
65        let elsewhere = DateTime::<Utc>::from(instant).with_timezone(&zone);
66        assert_eq!(Timestamp::try_from(elsewhere), Ok(instant), "read in UTC");
67    }
68
69    #[test]
70    fn an_instant_past_the_range_is_refused() {
71        assert_eq!(
72            Timestamp::try_from(DateTime::<Utc>::MAX_UTC),
73            Err(OutOfRangeError),
74            "after 2262"
75        );
76        assert_eq!(
77            Timestamp::try_from(DateTime::<Utc>::MIN_UTC),
78            Err(OutOfRangeError),
79            "before 1677"
80        );
81    }
82
83    #[rstest]
84    #[case::the_longest_backwards(Timedelta::MIN)]
85    #[case::a_budget(Timedelta::from_millis(-250))]
86    #[case::the_longest(Timedelta::MAX)]
87    fn a_span_crosses_both_ways(#[case] span: Timedelta) {
88        let theirs = TimeDelta::from(span);
89        assert_eq!(theirs.num_nanoseconds(), Some(span.as_nanos()), "the same span");
90        assert_eq!(Timedelta::try_from(theirs), Ok(span), "and back");
91    }
92
93    #[test]
94    fn a_span_past_the_range_is_refused() {
95        assert_eq!(Timedelta::try_from(TimeDelta::MAX), Err(OutOfRangeError), "forwards");
96        assert_eq!(Timedelta::try_from(TimeDelta::MIN), Err(OutOfRangeError), "and backwards");
97    }
98}