spacetimedb_sats/
time_duration.rs

1use crate::timestamp::MICROSECONDS_PER_SECOND;
2use crate::{de::Deserialize, impl_st, ser::Serialize, AlgebraicType};
3use std::fmt;
4use std::time::Duration;
5
6#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
7#[sats(crate = crate)]
8/// A span or delta in time, measured in microseconds.
9///
10/// Analogous to [`std::time::Duration`], and to C#'s `TimeSpan`.
11/// Name chosen to avoid ambiguity with either of those types.
12///
13/// Unlike [`Duration`], but like C#'s `TimeSpan`,
14/// `TimeDuration` can represent negative values.
15/// It also offers less range than [`Duration`], so conversions in both directions may fail.
16pub struct TimeDuration {
17    __time_duration_micros__: i64,
18}
19
20impl_st!([] TimeDuration, AlgebraicType::time_duration());
21
22impl TimeDuration {
23    pub const ZERO: TimeDuration = TimeDuration {
24        __time_duration_micros__: 0,
25    };
26
27    /// Get the number of microseconds `self` represents.
28    pub fn to_micros(self) -> i64 {
29        self.__time_duration_micros__
30    }
31
32    /// Construct a [`TimeDuration`] which is `micros` microseconds.
33    ///
34    /// A positive value means a time after the Unix epoch,
35    /// and a negative value means a time before.
36    pub fn from_micros(micros: i64) -> Self {
37        Self {
38            __time_duration_micros__: micros,
39        }
40    }
41
42    /// Returns `Err(abs(self) as Duration)` if `self` is negative.
43    pub fn to_duration(self) -> Result<Duration, Duration> {
44        let micros = self.to_micros();
45        if micros >= 0 {
46            Ok(Duration::from_micros(micros as u64))
47        } else {
48            Err(Duration::from_micros((-micros) as u64))
49        }
50    }
51
52    /// Returns a `Duration` representing the absolute magnitude of `self`.
53    ///
54    /// Regardless of whether `self` is positive or negative, the returned `Duration` is positive.
55    pub fn to_duration_abs(self) -> Duration {
56        match self.to_duration() {
57            Ok(dur) | Err(dur) => dur,
58        }
59    }
60
61    /// Return a [`TimeDuration`] which represents the same span as `duration`.
62    ///
63    /// Panics if `duration.as_micros` overflows an `i64`
64    pub fn from_duration(duration: Duration) -> Self {
65        Self::from_micros(
66            duration
67                .as_micros()
68                .try_into()
69                .expect("Duration overflows i64 microseconds"),
70        )
71    }
72}
73
74impl From<Duration> for TimeDuration {
75    fn from(d: Duration) -> TimeDuration {
76        TimeDuration::from_duration(d)
77    }
78}
79
80impl TryFrom<TimeDuration> for Duration {
81    type Error = Duration;
82    /// If `d` is negative, returns its magnitude as the `Err` variant.
83    fn try_from(d: TimeDuration) -> Result<Duration, Duration> {
84        d.to_duration()
85    }
86}
87
88impl fmt::Display for TimeDuration {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        let micros = self.to_micros();
91        let sign = if micros < 0 { "-" } else { "+" };
92        let pos = micros.abs();
93        let secs = pos / MICROSECONDS_PER_SECOND;
94        let micros_remaining = pos % MICROSECONDS_PER_SECOND;
95        write!(f, "{sign}{secs}.{micros_remaining:06}")
96    }
97}
98
99#[cfg(test)]
100mod test {
101    use super::*;
102    use crate::GroundSpacetimeType;
103    use proptest::prelude::*;
104    use std::time::SystemTime;
105
106    #[test]
107    fn timestamp_type_matches() {
108        assert_eq!(AlgebraicType::time_duration(), TimeDuration::get_type());
109        assert!(TimeDuration::get_type().is_time_duration());
110        assert!(TimeDuration::get_type().is_special());
111    }
112
113    #[test]
114    fn round_trip_duration_through_time_duration() {
115        let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
116        let rounded = Duration::from_micros(now.as_micros() as _);
117        let time_duration = TimeDuration::from_duration(rounded);
118        let now_prime = time_duration.to_duration().unwrap();
119        assert_eq!(rounded, now_prime);
120    }
121
122    proptest! {
123        #[test]
124        fn round_trip_time_duration_through_systemtime(micros in any::<i64>().prop_map(|n| n.abs())) {
125            let time_duration = TimeDuration::from_micros(micros);
126            let duration = time_duration.to_duration().unwrap();
127            let time_duration_prime = TimeDuration::from_duration(duration);
128            prop_assert_eq!(time_duration_prime, time_duration);
129            prop_assert_eq!(time_duration_prime.to_micros(), micros);
130        }
131    }
132}