spacetimedb_sats/
time_duration.rs

1use crate::timestamp::MICROSECONDS_PER_SECOND;
2use crate::{de::Deserialize, impl_st, ser::Serialize, AlgebraicType, AlgebraicValue};
3use std::fmt;
4use std::ops::{Add, AddAssign, Sub, SubAssign};
5use std::time::Duration;
6
7#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
8#[sats(crate = crate)]
9/// A span or delta in time, measured in microseconds.
10///
11/// Analogous to [`std::time::Duration`], and to C#'s `TimeSpan`.
12/// Name chosen to avoid ambiguity with either of those types.
13///
14/// Unlike [`Duration`], but like C#'s `TimeSpan`,
15/// `TimeDuration` can represent negative values.
16/// It also offers less range than [`Duration`], so conversions in both directions may fail.
17pub struct TimeDuration {
18    __time_duration_micros__: i64,
19}
20
21impl_st!([] TimeDuration, AlgebraicType::time_duration());
22
23impl TimeDuration {
24    pub const ZERO: TimeDuration = TimeDuration {
25        __time_duration_micros__: 0,
26    };
27
28    /// Get the number of microseconds `self` represents.
29    pub fn to_micros(self) -> i64 {
30        self.__time_duration_micros__
31    }
32
33    /// Construct a [`TimeDuration`] which is `micros` microseconds.
34    pub fn from_micros(micros: i64) -> Self {
35        Self {
36            __time_duration_micros__: micros,
37        }
38    }
39
40    /// Returns `Err(abs(self) as Duration)` if `self` is negative.
41    pub fn to_duration(self) -> Result<Duration, Duration> {
42        let micros = self.to_micros();
43        if micros >= 0 {
44            Ok(Duration::from_micros(micros as u64))
45        } else {
46            Err(Duration::from_micros((-micros) as u64))
47        }
48    }
49
50    /// Returns a `Duration` representing the absolute magnitude of `self`.
51    ///
52    /// Regardless of whether `self` is positive or negative, the returned `Duration` is positive.
53    pub fn to_duration_abs(self) -> Duration {
54        match self.to_duration() {
55            Ok(dur) | Err(dur) => dur,
56        }
57    }
58
59    /// Returns a positive `TimeDuration` with the magnitude of `self`.
60    pub fn abs(self) -> Self {
61        Self::from_micros(self.to_micros().saturating_abs())
62    }
63
64    /// Return a [`TimeDuration`] which represents the same span as `duration`.
65    ///
66    /// Panics if `duration.as_micros` overflows an `i64`
67    pub fn from_duration(duration: Duration) -> Self {
68        Self::from_micros(
69            duration
70                .as_micros()
71                .try_into()
72                .expect("Duration overflows i64 microseconds"),
73        )
74    }
75
76    /// Returns `Some(self + other)`, or `None` if that value would be out of bounds for [`TimeDuration`].
77    pub fn checked_add(self, other: Self) -> Option<Self> {
78        self.to_micros().checked_add(other.to_micros()).map(Self::from_micros)
79    }
80
81    /// Returns `Some(self - other)`, or `None` if that value would be out of bounds for [`TimeDuration`].
82    pub fn checked_sub(self, other: Self) -> Option<Self> {
83        self.to_micros().checked_sub(other.to_micros()).map(Self::from_micros)
84    }
85}
86
87impl From<Duration> for TimeDuration {
88    fn from(d: Duration) -> TimeDuration {
89        TimeDuration::from_duration(d)
90    }
91}
92
93impl TryFrom<TimeDuration> for Duration {
94    type Error = Duration;
95    /// If `d` is negative, returns its magnitude as the `Err` variant.
96    fn try_from(d: TimeDuration) -> Result<Duration, Duration> {
97        d.to_duration()
98    }
99}
100
101impl fmt::Display for TimeDuration {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        let micros = self.to_micros();
104        let sign = if micros < 0 { "-" } else { "+" };
105        let pos = micros.abs();
106        let secs = pos / MICROSECONDS_PER_SECOND;
107        let micros_remaining = pos % MICROSECONDS_PER_SECOND;
108        write!(f, "{sign}{secs}.{micros_remaining:06}")
109    }
110}
111
112impl Add for TimeDuration {
113    type Output = Self;
114
115    fn add(self, rhs: Self) -> Self::Output {
116        self.checked_add(rhs).unwrap()
117    }
118}
119
120impl Sub for TimeDuration {
121    type Output = Self;
122
123    fn sub(self, rhs: Self) -> Self::Output {
124        self.checked_sub(rhs).unwrap()
125    }
126}
127
128impl AddAssign for TimeDuration {
129    fn add_assign(&mut self, rhs: Self) {
130        *self = *self + rhs;
131    }
132}
133
134impl SubAssign for TimeDuration {
135    fn sub_assign(&mut self, rhs: Self) {
136        *self = *self - rhs;
137    }
138}
139
140// `std::time::Duration` has implementations of `Mul<u32>` and `Div<u32>`,
141// plus checked methods and assign traits.
142// It also has methods for division with floats,
143// both `Duration -> Duration -> float` and `Duration -> float -> Duration`.
144// We could provide some or all of these, but so far have not seen the need to.
145
146impl From<TimeDuration> for AlgebraicValue {
147    fn from(value: TimeDuration) -> Self {
148        AlgebraicValue::product([value.to_micros().into()])
149    }
150}
151
152#[cfg(test)]
153mod test {
154    use super::*;
155    use crate::GroundSpacetimeType;
156    use proptest::prelude::*;
157    use std::time::SystemTime;
158
159    #[test]
160    fn timestamp_type_matches() {
161        assert_eq!(AlgebraicType::time_duration(), TimeDuration::get_type());
162        assert!(TimeDuration::get_type().is_time_duration());
163        assert!(TimeDuration::get_type().is_special());
164    }
165
166    #[test]
167    fn round_trip_duration_through_time_duration() {
168        let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
169        let rounded = Duration::from_micros(now.as_micros() as _);
170        let time_duration = TimeDuration::from_duration(rounded);
171        let now_prime = time_duration.to_duration().unwrap();
172        assert_eq!(rounded, now_prime);
173    }
174
175    proptest! {
176        #[test]
177        fn round_trip_time_duration_through_systemtime(micros in any::<i64>().prop_map(|n| n.abs())) {
178            let time_duration = TimeDuration::from_micros(micros);
179            let duration = time_duration.to_duration().unwrap();
180            let time_duration_prime = TimeDuration::from_duration(duration);
181            prop_assert_eq!(time_duration_prime, time_duration);
182            prop_assert_eq!(time_duration_prime.to_micros(), micros);
183        }
184
185        #[test]
186        fn arithmetic_as_expected(lhs in any::<i64>(), rhs in any::<i64>()) {
187            let lhs_time_duration = TimeDuration::from_micros(lhs);
188            let rhs_time_duration = TimeDuration::from_micros(rhs);
189
190            if let Some(sum) = lhs.checked_add(rhs) {
191                let sum_time_duration = lhs_time_duration.checked_add(rhs_time_duration);
192                prop_assert!(sum_time_duration.is_some());
193                prop_assert_eq!(sum_time_duration.unwrap().to_micros(), sum);
194
195                prop_assert_eq!((lhs_time_duration + rhs_time_duration).to_micros(), sum);
196
197                let mut sum_assign = lhs_time_duration;
198                sum_assign += rhs_time_duration;
199                prop_assert_eq!(sum_assign.to_micros(), sum);
200            } else {
201                prop_assert!(lhs_time_duration.checked_add(rhs_time_duration).is_none());
202            }
203
204            if let Some(diff) = lhs.checked_sub(rhs) {
205                let diff_time_duration = lhs_time_duration.checked_sub(rhs_time_duration);
206                prop_assert!(diff_time_duration.is_some());
207                prop_assert_eq!(diff_time_duration.unwrap().to_micros(), diff);
208
209                prop_assert_eq!((lhs_time_duration - rhs_time_duration).to_micros(), diff);
210
211                let mut diff_assign = lhs_time_duration;
212                diff_assign -= rhs_time_duration;
213                prop_assert_eq!(diff_assign.to_micros(), diff);
214            } else {
215                prop_assert!(lhs_time_duration.checked_sub(rhs_time_duration).is_none());
216            }
217        }
218    }
219}