Skip to main content

t2t_core/interop/
time.rs

1//! [`Timestamp`] and [`Timedelta`] to and from time's `OffsetDateTime` and `Duration`.
2
3use time::{Duration, OffsetDateTime};
4
5use crate::errors::narrow;
6use crate::{OutOfRangeError, Timedelta, Timestamp};
7
8/// Every timestamp, in UTC, since time's reach years -9999 to 9999.
9impl From<Timestamp> for OffsetDateTime {
10    #[inline]
11    fn from(instant: Timestamp) -> Self {
12        Self::UNIX_EPOCH.saturating_add(Duration::nanoseconds(instant.0))
13    }
14}
15
16/// Refuses an instant outside [`Timestamp::MIN`] to [`Timestamp::MAX`], at any offset.
17impl TryFrom<OffsetDateTime> for Timestamp {
18    type Error = OutOfRangeError;
19
20    #[inline]
21    fn try_from(instant: OffsetDateTime) -> Result<Self, Self::Error> {
22        narrow(instant.unix_timestamp_nanos()).map(Self)
23    }
24}
25
26/// Every span, since a `Duration` holds every `i64` of nanoseconds.
27impl From<Timedelta> for Duration {
28    #[inline]
29    fn from(span: Timedelta) -> Self {
30        Self::nanoseconds(span.0)
31    }
32}
33
34/// Refuses a duration past [`Timedelta::MIN`] or [`Timedelta::MAX`].
35impl TryFrom<Duration> for Timedelta {
36    type Error = OutOfRangeError;
37
38    #[inline]
39    fn try_from(duration: Duration) -> Result<Self, Self::Error> {
40        narrow(duration.whole_nanoseconds()).map(Self)
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use rstest::rstest;
47    use time::{Duration, OffsetDateTime, UtcOffset};
48
49    use crate::{OutOfRangeError, Timedelta, Timestamp};
50
51    #[rstest]
52    #[case::the_earliest(Timestamp::MIN)]
53    #[case::before_the_epoch(Timestamp::from_nanos(-1))]
54    #[case::a_capture(Timestamp::from_nanos(1_789_544_735_123_456_789))]
55    #[case::the_latest(Timestamp::MAX)]
56    fn an_instant_crosses_both_ways(#[case] instant: Timestamp) {
57        let theirs = OffsetDateTime::from(instant);
58        assert_eq!(
59            theirs.unix_timestamp_nanos(),
60            i128::from(instant.as_nanos()),
61            "the same instant"
62        );
63        assert_eq!(theirs.offset(), UtcOffset::UTC, "in UTC");
64        assert_eq!(Timestamp::try_from(theirs), Ok(instant), "and back");
65    }
66
67    #[test]
68    fn an_instant_at_another_offset_is_the_same_instant() {
69        let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
70        let offset = UtcOffset::from_hms(5, 0, 0).expect("five hours east is an offset");
71        let elsewhere = OffsetDateTime::from(instant).to_offset(offset);
72        assert_eq!(Timestamp::try_from(elsewhere), Ok(instant), "read in UTC");
73    }
74
75    #[test]
76    fn an_instant_past_the_range_is_refused() {
77        let far_future = OffsetDateTime::UNIX_EPOCH.saturating_add(Duration::MAX);
78        let distant_past = OffsetDateTime::UNIX_EPOCH.saturating_add(Duration::MIN);
79        assert_eq!(Timestamp::try_from(far_future), Err(OutOfRangeError), "after 2262");
80        assert_eq!(Timestamp::try_from(distant_past), Err(OutOfRangeError), "before 1677");
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 = Duration::from(span);
89        assert_eq!(theirs.whole_nanoseconds(), i128::from(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(Duration::MAX), Err(OutOfRangeError), "forwards");
96        assert_eq!(Timedelta::try_from(Duration::MIN), Err(OutOfRangeError), "and backwards");
97    }
98}